Gisle Aune
4 years ago
commit
2240985aa1
120 changed files with 11574 additions and 0 deletions
-
99.drone.yml
-
2.gitignore
-
21api/common.go
-
141api/goal.go
-
90api/group.go
-
98api/item.go
-
115api/log.go
-
101api/project.go
-
111api/task.go
-
51cmd/stufflog2-lambda/main.go
-
74cmd/stufflog2-local/main.go
-
28database/database.go
-
51database/postgres/db.go
-
115database/postgres/goals.go
-
96database/postgres/group.go
-
100database/postgres/item.go
-
108database/postgres/logs.go
-
104database/postgres/project.go
-
109database/postgres/tasks.go
-
20go.mod
-
333go.sum
-
71internal/auth/auth.go
-
52internal/generate/ids.go
-
18internal/slerrors/badrequest.go
-
22internal/slerrors/forbidden.go
-
35internal/slerrors/gin.go
-
18internal/slerrors/notfound.go
-
15migrations/postgres/20201218170259_create_table_group.sql
-
16migrations/postgres/20201218170301_create_table_item.sql
-
18migrations/postgres/20201218170319_create_table_project.sql
-
20migrations/postgres/20201218170338_create_table_task.sql
-
16migrations/postgres/20201218170348_create_table_log.sql
-
18migrations/postgres/20201218170417_create_table_goal.sql
-
9migrations/postgres/20201223121327_create_index_item_group_id.sql
-
9migrations/postgres/20201223125438_create_index_log_task_id.sql
-
9migrations/postgres/20201223125556_create_index_goal_start_time.sql
-
9migrations/postgres/20201223125559_create_index_goal_end_time.sql
-
9migrations/postgres/20201223125934_create_index_group_user_id.sql
-
9migrations/postgres/20201223125938_create_index_item_user_id.sql
-
9migrations/postgres/20201223125947_create_index_project_user_id.sql
-
9migrations/postgres/20201223125957_create_index_task_user_id.sql
-
9migrations/postgres/20201223130003_create_index_log_user_id.sql
-
9migrations/postgres/20201223130007_create_index_goal_user_id.sql
-
9migrations/postgres/20201223135724_create_index_project_created_time.sql
-
9migrations/postgres/20201223135812_create_index_task_item_id.sql
-
9migrations/postgres/20201223140113_create_index_log_logged_time.sql
-
9migrations/postgres/20201225175922_create_index_log_item_id.sql
-
73models/goal.go
-
47models/group.go
-
50models/item.go
-
50models/log.go
-
68models/project.go
-
74models/task.go
-
115serverless.yml
-
508services/loader.go
-
7svelte-ui/.gitignore
-
4368svelte-ui/package-lock.json
-
43svelte-ui/package.json
-
BINsvelte-ui/public/favicon.png
-
62svelte-ui/public/global.css
-
17svelte-ui/public/index.html
-
99svelte-ui/rollup.config.js
-
77svelte-ui/src/App.svelte
-
46svelte-ui/src/clients/amplify.ts
-
260svelte-ui/src/clients/stufflog.ts
-
53svelte-ui/src/components/Boi.svelte
-
17svelte-ui/src/components/DateSpan.svelte
-
87svelte-ui/src/components/DaysLeft.svelte
-
93svelte-ui/src/components/GoalEntry.svelte
-
84svelte-ui/src/components/GroupEntry.svelte
-
30svelte-ui/src/components/GroupSelect.svelte
-
22svelte-ui/src/components/Icon.svelte
-
56svelte-ui/src/components/IconSelect.svelte
-
86svelte-ui/src/components/ItemEntry.svelte
-
31svelte-ui/src/components/ItemSelect.svelte
-
95svelte-ui/src/components/LogEntry.svelte
-
44svelte-ui/src/components/Menu.svelte
-
215svelte-ui/src/components/Modal.svelte
-
10svelte-ui/src/components/ModalRoute.svelte
-
38svelte-ui/src/components/Option.svelte
-
10svelte-ui/src/components/OptionRow.svelte
-
80svelte-ui/src/components/Progress.svelte
-
94svelte-ui/src/components/ProjectEntry.svelte
-
161svelte-ui/src/components/TaskEntry.svelte
-
138svelte-ui/src/external/icons.ts
-
112svelte-ui/src/forms/GoalForm.svelte
-
90svelte-ui/src/forms/GroupForm.svelte
-
54svelte-ui/src/forms/ItemAddForm.svelte
-
50svelte-ui/src/forms/ItemDeleteForm.svelte
-
56svelte-ui/src/forms/ItemEditForm.svelte
-
71svelte-ui/src/forms/LogAddForm.svelte
-
56svelte-ui/src/forms/LogDeleteForm.svelte
-
59svelte-ui/src/forms/LogEditForm.svelte
-
74svelte-ui/src/forms/LoginForm.svelte
-
54svelte-ui/src/forms/ProjectAddForm.svelte
-
56svelte-ui/src/forms/ProjectDeleteForm.svelte
-
60svelte-ui/src/forms/ProjectEditForm.svelte
-
72svelte-ui/src/forms/TaskAddForm.svelte
-
60svelte-ui/src/forms/TaskDeleteForm.svelte
-
63svelte-ui/src/forms/TaskEditForm.svelte
@ -0,0 +1,99 @@ |
|||
name: lektura-red |
|||
|
|||
kind: pipeline |
|||
type: docker |
|||
|
|||
steps: |
|||
- name: backend-build |
|||
image: golang:1.15 |
|||
depends_on: [] |
|||
commands: |
|||
- go mod download |
|||
- CGO_ENABLED=0 go build -ldflags "-w -s" -o build/api/handler cmd/stufflog2-lambda/main.go |
|||
|
|||
- name: backend-test |
|||
image: golang:1.15 |
|||
depends_on: [] |
|||
commands: |
|||
- go test -v ./... |
|||
|
|||
- name: backend-migrate |
|||
image: golang:1.15 |
|||
depends_on: [] |
|||
environment: |
|||
DB_CONNECT: |
|||
from:secret: db_connect |
|||
commands: |
|||
- go get -u github.com/pressly/goose/... |
|||
- cd migrations/postgres |
|||
- goose postgres "$DB_CONNECT" up |
|||
|
|||
- name: backend-deploy |
|||
image: node:14.14.0 |
|||
depends_on: |
|||
- backend-build |
|||
- backend-test |
|||
- backend-migrate |
|||
environment: |
|||
AWS_ACCESS_KEY_ID: |
|||
from_secret: aws_access_key_id |
|||
AWS_SECRET_ACCESS_KEY: |
|||
from_secret: aws_secret_access_key |
|||
AWS_DEFAULT_REGION: |
|||
from_secret: aws_region |
|||
AMI_ROLE: |
|||
from_secret: ami_role |
|||
S3_WEBUI_BUCKET: |
|||
from_secret: s3_webui_bucket |
|||
DOMAIN_NAME: |
|||
from_secret: domain_name |
|||
CERTIFICATE_NAME: |
|||
from_secret: certificate_name |
|||
CERTIFICATE_ARN: |
|||
from_secret: certificate_arn |
|||
HOSTED_ZONE_ID: |
|||
from_secret: hosted_zone_id |
|||
DB_DRIVER: |
|||
from_secret: db_driver |
|||
DB_CONNECT: |
|||
from:secret: db_connect |
|||
commands: |
|||
- apt-get update > /dev/null 2>&1 |
|||
- apt-get -y install awscli zip > /dev/null 2>&1 |
|||
- npm install -g serverless > /dev/null 2>&1 |
|||
- npm install -g serverless-domain-manager > /dev/null 2>&1 |
|||
- npm install -g serverless-apigateway-service-proxy > /dev/null 2>&1 |
|||
- serverless deploy |
|||
|
|||
- name: frontend-build |
|||
image: node:14.14.0 |
|||
depends_on: [] |
|||
environment: |
|||
AWS_AMPLIFY_REGION: |
|||
from_secret: aws_region |
|||
AWS_AMPLIFY_USER_POOL_ID: |
|||
from_secret: aws_amplify_user_pool_id |
|||
AWS_AMPLIFY_USER_POOL_WEB_CLIENT_ID: |
|||
from_secret: aws_amplify_user_pool_web_client_id |
|||
|
|||
commands: |
|||
- cd svelte-ui |
|||
- npm install |
|||
- npm run build |
|||
|
|||
- name: frontend-deploy |
|||
image: amazon/aws-cli:latest |
|||
depends_on: |
|||
- frontend-build |
|||
environment: |
|||
AWS_ACCESS_KEY_ID: |
|||
from_secret: aws_access_key_id |
|||
AWS_SECRET_ACCESS_KEY: |
|||
from_secret: aws_secret_access_key |
|||
AWS_DEFAULT_REGION: |
|||
from_secret: aws_region |
|||
S3_WEBUI_BUCKET: |
|||
from_secret: s3_webui_bucket |
|||
commands: |
|||
- cd webui/build |
|||
- aws s3 sync . s3://$S3_WEBUI_BUCKET |
@ -0,0 +1,2 @@ |
|||
/stufflog2-* |
|||
/.idea |
@ -0,0 +1,21 @@ |
|||
package api |
|||
|
|||
import ( |
|||
"github.com/gin-gonic/gin" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
) |
|||
|
|||
func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin.HandlerFunc { |
|||
return func(c *gin.Context) { |
|||
res, err := callback(c) |
|||
if err != nil { |
|||
slerrors.Respond(c, err) |
|||
return |
|||
} |
|||
|
|||
resJson := make(map[string]interface{}, 1) |
|||
resJson[key] = res |
|||
|
|||
c.JSON(200, resJson) |
|||
} |
|||
} |
@ -0,0 +1,141 @@ |
|||
package api |
|||
|
|||
import ( |
|||
"github.com/gin-gonic/gin" |
|||
"github.com/gissleh/stufflog/database" |
|||
"github.com/gissleh/stufflog/internal/auth" |
|||
"github.com/gissleh/stufflog/internal/generate" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/gissleh/stufflog/services" |
|||
"time" |
|||
) |
|||
|
|||
func Goal(g *gin.RouterGroup, db database.Database) { |
|||
l := services.Loader{DB: db} |
|||
|
|||
g.GET("/", handler("goals", func(c *gin.Context) (interface{}, error) { |
|||
filter := models.GoalFilter{} |
|||
if value := c.Query("minTime"); value != "" { |
|||
minTime, err := time.Parse(time.RFC3339Nano, value) |
|||
if err != nil { |
|||
return nil, slerrors.BadRequest("Invalid minTime") |
|||
} |
|||
minTime = minTime.UTC() |
|||
|
|||
filter.MinTime = &minTime |
|||
} else { |
|||
lastMonth := time.Now().Add(-30 * 24 * time.Hour).UTC() |
|||
filter.MinTime = &lastMonth |
|||
} |
|||
if value := c.Query("maxTime"); value != "" { |
|||
maxTime, err := time.Parse(time.RFC3339Nano, value) |
|||
if err != nil { |
|||
return nil, slerrors.BadRequest("Invalid maxTime") |
|||
} |
|||
maxTime = maxTime.UTC() |
|||
|
|||
filter.MaxTime = &maxTime |
|||
} |
|||
if value := c.Query("includesTime"); value != "" { |
|||
includesTime, err := time.Parse(time.RFC3339Nano, value) |
|||
if err != nil { |
|||
return nil, slerrors.BadRequest("Invalid includesTime") |
|||
} |
|||
includesTime = includesTime.UTC() |
|||
|
|||
filter.IncludesTime = &includesTime |
|||
} |
|||
|
|||
return l.ListGoals(c.Request.Context(), filter) |
|||
})) |
|||
|
|||
g.GET("/:id", handler("goal", func(c *gin.Context) (interface{}, error) { |
|||
return l.FindGoal(c.Request.Context(), c.Param("id")) |
|||
})) |
|||
|
|||
g.POST("/", handler("goal", func(c *gin.Context) (interface{}, error) { |
|||
goal := models.Goal{} |
|||
err := c.BindJSON(&goal) |
|||
if err != nil { |
|||
return nil, slerrors.BadRequest("Invalid JSON") |
|||
} |
|||
goal.ID = generate.GoalID() |
|||
goal.UserID = auth.UserID(c) |
|||
|
|||
if goal.Amount <= 0 { |
|||
return nil, slerrors.BadRequest("Goal amount must be more than 0.") |
|||
} |
|||
if goal.StartTime.IsZero() { |
|||
return nil, slerrors.BadRequest("Start time must be set.") |
|||
} |
|||
if goal.EndTime.IsZero() { |
|||
return nil, slerrors.BadRequest("End time must be set.") |
|||
} |
|||
if !goal.EndTime.After(goal.StartTime) { |
|||
return nil, slerrors.BadRequest("Start time must be before end time.") |
|||
} |
|||
|
|||
goal.StartTime = goal.StartTime.UTC() |
|||
goal.EndTime = goal.EndTime.UTC() |
|||
|
|||
group, err := db.Groups().Find(c.Request.Context(), goal.GroupID) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
goal.GroupID = group.ID |
|||
|
|||
err = db.Goals().Insert(c.Request.Context(), goal) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return l.FindGoal(c.Request.Context(), goal.ID) |
|||
})) |
|||
|
|||
g.PUT("/:id", handler("goal", func(c *gin.Context) (interface{}, error) { |
|||
update := models.GoalUpdate{} |
|||
err := c.BindJSON(&update) |
|||
if err != nil { |
|||
return nil, slerrors.BadRequest("Invalid JSON") |
|||
} |
|||
|
|||
goal, err := l.FindGoal(c.Request.Context(), c.Param("id")) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
goal.StartTime = goal.StartTime.UTC() |
|||
goal.EndTime = goal.EndTime.UTC() |
|||
|
|||
goal.Update(update) |
|||
|
|||
if goal.Amount <= 0 { |
|||
return nil, slerrors.BadRequest("Goal amount must be more than 0.") |
|||
} |
|||
if !goal.EndTime.After(goal.StartTime) { |
|||
return nil, slerrors.BadRequest("Start time must be before end time.") |
|||
} |
|||
|
|||
err = db.Goals().Update(c.Request.Context(), goal.Goal) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return goal, nil |
|||
})) |
|||
|
|||
g.DELETE("/:id", handler("goal", func(c *gin.Context) (interface{}, error) { |
|||
goal, err := l.FindGoal(c.Request.Context(), c.Param("id")) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
err = db.Goals().Delete(c.Request.Context(), goal.Goal) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return goal, nil |
|||
})) |
|||
} |
@ -0,0 +1,90 @@ |
|||
package api |
|||
|
|||
import ( |
|||
"github.com/gin-gonic/gin" |
|||
"github.com/gissleh/stufflog/database" |
|||
"github.com/gissleh/stufflog/internal/auth" |
|||
"github.com/gissleh/stufflog/internal/generate" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/gissleh/stufflog/services" |
|||
) |
|||
|
|||
func Group(g *gin.RouterGroup, db database.Database) { |
|||
l := services.Loader{DB: db} |
|||
|
|||
g.GET("/", handler("groups", func(c *gin.Context) (interface{}, error) { |
|||
return l.ListGroups(c, models.GroupFilter{}) |
|||
})) |
|||
|
|||
g.GET("/:id", handler("group", func(c *gin.Context) (interface{}, error) { |
|||
return l.FindGroup(c, c.Param("id")) |
|||
})) |
|||
|
|||
g.POST("/", handler("group", func(c *gin.Context) (interface{}, error) { |
|||
group := models.Group{} |
|||
err := c.BindJSON(&group) |
|||
if err != nil { |
|||
return nil, slerrors.BadRequest("Invalid JSON") |
|||
} |
|||
group.ID = generate.GroupID() |
|||
group.UserID = auth.UserID(c) |
|||
|
|||
err = db.Groups().Insert(c.Request.Context(), group) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return &models.GroupResult{ |
|||
Group: group, |
|||
Items: []*models.Item{}, |
|||
}, nil |
|||
})) |
|||
|
|||
g.PUT("/:id", handler("group", func(c *gin.Context) (interface{}, error) { |
|||
update := models.GroupUpdate{} |
|||
err := c.BindJSON(&update) |
|||
if err != nil { |
|||
return nil, slerrors.BadRequest("Invalid JSON") |
|||
} |
|||
|
|||
group, err := db.Groups().Find(c.Request.Context(), c.Param("id")) |
|||
if err != nil { |
|||
return nil, err |
|||
} else if auth.UserID(c) != group.UserID { |
|||
return nil, slerrors.NotFound("Item") |
|||
} |
|||
|
|||
group.Update(update) |
|||
err = db.Groups().Update(c.Request.Context(), *group) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return l.FindGroup(c, group.ID) |
|||
})) |
|||
|
|||
g.DELETE("/:id", handler("group", func(c *gin.Context) (interface{}, error) { |
|||
group, err := l.FindGroup(c, c.Param("id")) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if len(group.Items) > 0 { |
|||
return nil, slerrors.Forbidden("cannot delete non-empty group.") |
|||
} |
|||
goals, err := db.Goals().List(c.Request.Context(), models.GoalFilter{GroupIDs: []string{group.ID}}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if len(goals) > 0 { |
|||
return nil, slerrors.Forbidden("cannot delete group with goals.") |
|||
} |
|||
|
|||
err = db.Groups().Delete(c.Request.Context(), group.Group) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return group, nil |
|||
})) |
|||
} |
@ -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 |
|||
})) |
|||
} |
@ -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 |
|||
})) |
|||
} |
@ -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 |
|||
})) |
|||
} |
@ -0,0 +1,111 @@ |
|||
package api |
|||
|
|||
import ( |
|||
"github.com/gin-gonic/gin" |
|||
"github.com/gissleh/stufflog/database" |
|||
"github.com/gissleh/stufflog/internal/auth" |
|||
"github.com/gissleh/stufflog/internal/generate" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/gissleh/stufflog/services" |
|||
"time" |
|||
) |
|||
|
|||
func Task(g *gin.RouterGroup, db database.Database) { |
|||
l := services.Loader{DB: db} |
|||
|
|||
defaultActive := true |
|||
|
|||
g.GET("/", handler("tasks", func(c *gin.Context) (interface{}, error) { |
|||
filter := models.TaskFilter{} |
|||
if setting := c.Query("active"); setting != "" { |
|||
active := setting == "true" |
|||
filter.Active = &active |
|||
} else { |
|||
filter.Active = &defaultActive |
|||
} |
|||
|
|||
return l.ListTasks(c, filter) |
|||
})) |
|||
|
|||
g.GET("/:id", handler("task", func(c *gin.Context) (interface{}, error) { |
|||
return l.FindTask(c, c.Param("id")) |
|||
})) |
|||
|
|||
g.POST("/", handler("task", func(c *gin.Context) (interface{}, error) { |
|||
task := models.Task{} |
|||
err := c.BindJSON(&task) |
|||
if err != nil { |
|||
return nil, slerrors.BadRequest("Invalid JSON") |
|||
} |
|||
if task.EndTime != nil && task.EndTime.Before(time.Now()) { |
|||
return nil, slerrors.BadRequest("Project end time must be later than current time.") |
|||
} |
|||
|
|||
project, err := l.FindProject(c.Request.Context(), task.ProjectID) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
item, err := l.FindItem(c.Request.Context(), task.ItemID) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
task.ID = generate.TaskID() |
|||
task.UserID = auth.UserID(c) |
|||
task.CreatedTime = time.Now().UTC() |
|||
task.ItemID = item.ID |
|||
task.ProjectID = project.ID |
|||
|
|||
err = db.Tasks().Insert(c.Request.Context(), task) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return &models.TaskResult{ |
|||
Task: task, |
|||
Logs: []*models.Log{}, |
|||
Item: &item.Item, |
|||
CompletedAmount: 0, |
|||
}, nil |
|||
})) |
|||
|
|||
g.PUT("/:id", handler("task", func(c *gin.Context) (interface{}, error) { |
|||
update := models.TaskUpdate{} |
|||
err := c.BindJSON(&update) |
|||
if err != nil { |
|||
return nil, slerrors.BadRequest("Invalid JSON") |
|||
} |
|||
|
|||
task, err := l.FindTask(c.Request.Context(), c.Param("id")) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
task.Update(update) |
|||
err = db.Tasks().Update(c.Request.Context(), task.Task) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return task, nil |
|||
})) |
|||
|
|||
g.DELETE("/:id", handler("task", func(c *gin.Context) (interface{}, error) { |
|||
task, err := l.FindTask(c.Request.Context(), c.Param("id")) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
if len(task.Logs) > 0 { |
|||
return nil, slerrors.Forbidden("cannot delete tasks with logs.") |
|||
} |
|||
|
|||
err = db.Tasks().Delete(c.Request.Context(), task.Task) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return task, nil |
|||
})) |
|||
} |
@ -0,0 +1,51 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"context" |
|||
"github.com/aws/aws-lambda-go/events" |
|||
"github.com/aws/aws-lambda-go/lambda" |
|||
ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin" |
|||
"github.com/gin-gonic/gin" |
|||
"github.com/gissleh/stufflog/api" |
|||
"github.com/gissleh/stufflog/database" |
|||
"github.com/gissleh/stufflog/internal/auth" |
|||
"log" |
|||
"os" |
|||
) |
|||
|
|||
func env(key string, defaultValue string) string { |
|||
e := os.Getenv(key) |
|||
if e == "" { |
|||
return defaultValue |
|||
} |
|||
|
|||
return e |
|||
} |
|||
|
|||
func main() { |
|||
dbDriver := env("DB_DRIVER", "postgres") |
|||
dbConnect := env("DB_CONNECT", "") |
|||
|
|||
gin.SetMode(gin.ReleaseMode) |
|||
|
|||
db, err := database.Open(context.Background(), dbDriver, dbConnect) |
|||
if err != nil { |
|||
log.Println("Failed to open database:", err) |
|||
os.Exit(1) |
|||
} |
|||
|
|||
server := gin.New() |
|||
server.Use(auth.TrustingJwtParserMiddleware()) |
|||
|
|||
api.Group(server.Group("/api/group"), db) |
|||
api.Item(server.Group("/api/item"), db) |
|||
api.Project(server.Group("/api/project"), db) |
|||
api.Task(server.Group("/api/task"), db) |
|||
api.Log(server.Group("/api/log"), db) |
|||
api.Goal(server.Group("/api/goal"), db) |
|||
|
|||
ginLambda := ginadapter.New(server) |
|||
lambda.Start(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { |
|||
return ginLambda.ProxyWithContext(ctx, request) |
|||
}) |
|||
} |
@ -0,0 +1,74 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"context" |
|||
"github.com/gin-gonic/gin" |
|||
"github.com/gissleh/stufflog/api" |
|||
"github.com/gissleh/stufflog/database" |
|||
"github.com/gissleh/stufflog/internal/auth" |
|||
"log" |
|||
"os" |
|||
"os/signal" |
|||
"syscall" |
|||
) |
|||
|
|||
func env(key string, defaultValue string) string { |
|||
e := os.Getenv(key) |
|||
if e == "" { |
|||
return defaultValue |
|||
} |
|||
|
|||
return e |
|||
} |
|||
|
|||
func main() { |
|||
dbDriver := env("DB_DRIVER", "postgres") |
|||
dbConnect := env("DB_CONNECT", "host=localhost user=stufflog2 password=stufflog2_dev_password dbname=stufflog2 sslmode=disable") |
|||
serverListen := env("SERVER_LISTEN", ":8000") |
|||
useDummyUuid := env("USE_DUMMY_UUID", "no") |
|||
dummyUuid := env("DUMMY_UUID", "9d3214f1-6321-403e-ab87-764ad1a1252d") |
|||
|
|||
db, err := database.Open(context.Background(), dbDriver, dbConnect) |
|||
if err != nil { |
|||
log.Println("Failed to open database:", err) |
|||
os.Exit(1) |
|||
} |
|||
|
|||
server := gin.New() |
|||
if useDummyUuid == "yes" { |
|||
server.Use(auth.DummyMiddleware(dummyUuid)) |
|||
} else { |
|||
server.Use(auth.TrustingJwtParserMiddleware()) |
|||
} |
|||
|
|||
api.Group(server.Group("/api/group"), db) |
|||
api.Item(server.Group("/api/item"), db) |
|||
api.Project(server.Group("/api/project"), db) |
|||
api.Task(server.Group("/api/task"), db) |
|||
api.Log(server.Group("/api/log"), db) |
|||
api.Goal(server.Group("/api/goal"), db) |
|||
|
|||
exitSignal := make(chan os.Signal) |
|||
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM) |
|||
|
|||
errCh := make(chan error) |
|||
go func() { |
|||
err := server.Run(serverListen) |
|||
if err != nil { |
|||
errCh <- err |
|||
} |
|||
}() |
|||
|
|||
select { |
|||
case sig := <-exitSignal: |
|||
{ |
|||
log.Println("Received signal", sig) |
|||
os.Exit(0) |
|||
} |
|||
case err := <-errCh: |
|||
{ |
|||
log.Println("Server run failed:", err) |
|||
os.Exit(2) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,28 @@ |
|||
package database |
|||
|
|||
import ( |
|||
"context" |
|||
"errors" |
|||
"github.com/gissleh/stufflog/database/postgres" |
|||
"github.com/gissleh/stufflog/models" |
|||
) |
|||
|
|||
var ErrUnsupportedDriver = errors.New("usupported driver") |
|||
|
|||
type Database interface { |
|||
Goals() models.GoalRepository |
|||
Groups() models.GroupRepository |
|||
Items() models.ItemRepository |
|||
Logs() models.LogRepository |
|||
Projects() models.ProjectRepository |
|||
Tasks() models.TaskRepository |
|||
} |
|||
|
|||
func Open(ctx context.Context, driver string, connect string) (Database, error) { |
|||
switch driver { |
|||
case "postgres": |
|||
return postgres.Setup(ctx, connect) |
|||
default: |
|||
return nil, ErrUnsupportedDriver |
|||
} |
|||
} |
@ -0,0 +1,51 @@ |
|||
package postgres |
|||
|
|||
import ( |
|||
"context" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/jmoiron/sqlx" |
|||
|
|||
_ "github.com/lib/pq" |
|||
) |
|||
|
|||
type Database struct { |
|||
db *sqlx.DB |
|||
} |
|||
|
|||
func (d *Database) Goals() models.GoalRepository { |
|||
return &goalRepository{db: d.db} |
|||
} |
|||
|
|||
func (d *Database) Groups() models.GroupRepository { |
|||
return &groupRepository{db: d.db} |
|||
} |
|||
|
|||
func (d *Database) Items() models.ItemRepository { |
|||
return &itemRepository{db: d.db} |
|||
} |
|||
|
|||
func (d *Database) Logs() models.LogRepository { |
|||
return &logRepository{db: d.db} |
|||
} |
|||
|
|||
func (d *Database) Projects() models.ProjectRepository { |
|||
return &projectRepository{db: d.db} |
|||
} |
|||
|
|||
func (d *Database) Tasks() models.TaskRepository { |
|||
return &taskRepository{db: d.db} |
|||
} |
|||
|
|||
func Setup(ctx context.Context, connect string) (*Database, error) { |
|||
db, err := sqlx.ConnectContext(ctx, "postgres", connect) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
err = db.PingContext(ctx) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return &Database{db: db}, nil |
|||
} |
@ -0,0 +1,115 @@ |
|||
package postgres |
|||
|
|||
import ( |
|||
"context" |
|||
"database/sql" |
|||
"github.com/Masterminds/squirrel" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/jmoiron/sqlx" |
|||
) |
|||
|
|||
type goalRepository struct { |
|||
db *sqlx.DB |
|||
} |
|||
|
|||
func (r *goalRepository) Find(ctx context.Context, id string) (*models.Goal, error) { |
|||
res := models.Goal{} |
|||
err := r.db.GetContext(ctx, &res, "SELECT * FROM goal WHERE goal_id=$1", id) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return nil, slerrors.NotFound("Goal") |
|||
} |
|||
|
|||
return nil, err |
|||
} |
|||
|
|||
return &res, nil |
|||
} |
|||
|
|||
func (r *goalRepository) List(ctx context.Context, filter models.GoalFilter) ([]*models.Goal, error) { |
|||
sq := squirrel.Select("*").From("goal").PlaceholderFormat(squirrel.Dollar) |
|||
sq = sq.Where(squirrel.Eq{"user_id": filter.UserID}) |
|||
if len(filter.IDs) > 0 { |
|||
sq = sq.Where(squirrel.Eq{"goal_id": filter.IDs}) |
|||
} |
|||
if filter.MinTime != nil { |
|||
sq = sq.Where(squirrel.GtOrEq{ |
|||
"end_time": *filter.MinTime, |
|||
}) |
|||
} |
|||
if filter.MaxTime != nil { |
|||
sq = sq.Where(squirrel.LtOrEq{ |
|||
"start_time": *filter.MaxTime, |
|||
}) |
|||
} |
|||
if filter.IncludesTime != nil { |
|||
sq = sq.Where(squirrel.LtOrEq{ |
|||
"start_time": *filter.IncludesTime, |
|||
}).Where(squirrel.GtOrEq{ |
|||
"end_time": *filter.IncludesTime, |
|||
}) |
|||
} |
|||
|
|||
sq = sq.OrderBy("start_time", "end_time", "name") |
|||
|
|||
query, args, err := sq.ToSql() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
res := make([]*models.Goal, 0, 8) |
|||
err = r.db.SelectContext(ctx, &res, query, args...) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return res, nil |
|||
} |
|||
return nil, err |
|||
} |
|||
|
|||
return res, nil |
|||
} |
|||
|
|||
func (r *goalRepository) Insert(ctx context.Context, goal models.Goal) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
INSERT INTO goal ( |
|||
goal_id, user_id, group_id, amount, start_time, end_time, name, description |
|||
) VALUES ( |
|||
:goal_id, :user_id, :group_id, :amount, :start_time, :end_time, :name, :description |
|||
) |
|||
`, &goal) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *goalRepository) Update(ctx context.Context, goal models.Goal) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
UPDATE goal SET |
|||
amount=:amount, |
|||
start_time=:start_time, |
|||
end_time=:end_time, |
|||
name=:name, |
|||
description=:description |
|||
WHERE goal_id=:goal_id |
|||
`, &goal) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *goalRepository) Delete(ctx context.Context, goal models.Goal) error { |
|||
_, err := r.db.ExecContext(ctx, `DELETE FROM goal WHERE goal_id=$1`, goal.ID) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return slerrors.NotFound("Goal") |
|||
} |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,96 @@ |
|||
package postgres |
|||
|
|||
import ( |
|||
"context" |
|||
"database/sql" |
|||
"github.com/Masterminds/squirrel" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/jmoiron/sqlx" |
|||
) |
|||
|
|||
type groupRepository struct { |
|||
db *sqlx.DB |
|||
} |
|||
|
|||
func (r *groupRepository) Find(ctx context.Context, id string) (*models.Group, error) { |
|||
res := models.Group{} |
|||
err := r.db.GetContext(ctx, &res, "SELECT * FROM \"group\" WHERE group_id=$1", id) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return nil, slerrors.NotFound("Group") |
|||
} |
|||
|
|||
return nil, err |
|||
} |
|||
|
|||
return &res, nil |
|||
} |
|||
|
|||
func (r *groupRepository) List(ctx context.Context, filter models.GroupFilter) ([]*models.Group, error) { |
|||
sq := squirrel.Select("*").From("\"group\"").PlaceholderFormat(squirrel.Dollar) |
|||
sq = sq.Where(squirrel.Eq{"user_id": filter.UserID}) |
|||
if len(filter.IDs) > 0 { |
|||
sq = sq.Where(squirrel.Eq{"group_id": filter.IDs}) |
|||
} |
|||
|
|||
sq = sq.OrderBy("name") |
|||
|
|||
query, args, err := sq.ToSql() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
res := make([]*models.Group, 0, 8) |
|||
err = r.db.SelectContext(ctx, &res, query, args...) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return []*models.Group{}, nil |
|||
} |
|||
return nil, err |
|||
} |
|||
|
|||
return res, nil |
|||
} |
|||
|
|||
func (r *groupRepository) Insert(ctx context.Context, group models.Group) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
INSERT INTO "group" ( |
|||
group_id, user_id, name, icon, description |
|||
) VALUES ( |
|||
:group_id, :user_id, :name, :icon, :description |
|||
) |
|||
`, &group) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *groupRepository) Update(ctx context.Context, group models.Group) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
UPDATE "group" SET |
|||
name=:name, |
|||
icon=:icon, |
|||
description=:description |
|||
WHERE group_id=:group_id |
|||
`, &group) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *groupRepository) Delete(ctx context.Context, group models.Group) error { |
|||
_, err := r.db.ExecContext(ctx, `DELETE FROM "group" WHERE group_id=$1`, group.ID) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return slerrors.NotFound("Group") |
|||
} |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,100 @@ |
|||
package postgres |
|||
|
|||
import ( |
|||
"context" |
|||
"database/sql" |
|||
"github.com/Masterminds/squirrel" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/jmoiron/sqlx" |
|||
) |
|||
|
|||
type itemRepository struct { |
|||
db *sqlx.DB |
|||
} |
|||
|
|||
func (r *itemRepository) Find(ctx context.Context, id string) (*models.Item, error) { |
|||
res := models.Item{} |
|||
err := r.db.GetContext(ctx, &res, "SELECT item.*, g.icon FROM item INNER JOIN \"group\" AS g ON item.group_id = g.group_id WHERE item_id=$1", id) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return nil, slerrors.NotFound("Item") |
|||
} |
|||
|
|||
return nil, err |
|||
} |
|||
|
|||
return &res, nil |
|||
} |
|||
|
|||
func (r *itemRepository) List(ctx context.Context, filter models.ItemFilter) ([]*models.Item, error) { |
|||
sq := squirrel.Select("item.*", "g.icon").From("item").PlaceholderFormat(squirrel.Dollar) |
|||
sq = sq.Where(squirrel.Eq{"item.user_id": filter.UserID}) |
|||
if len(filter.IDs) > 0 { |
|||
sq = sq.Where(squirrel.Eq{"item.item_id": filter.IDs}) |
|||
} |
|||
if len(filter.GroupIDs) > 0 { |
|||
sq = sq.Where(squirrel.Eq{"item.group_id": filter.GroupIDs}) |
|||
} |
|||
|
|||
sq = sq.InnerJoin("\"group\" AS g ON item.group_id = g.group_id") |
|||
sq = sq.OrderBy("item.group_weight", "item.name") |
|||
|
|||
query, args, err := sq.ToSql() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
res := make([]*models.Item, 0, 8) |
|||
err = r.db.SelectContext(ctx, &res, query, args...) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return res, nil |
|||
} |
|||
return nil, err |
|||
} |
|||
|
|||
return res, nil |
|||
} |
|||
|
|||
func (r *itemRepository) Insert(ctx context.Context, item models.Item) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
INSERT INTO item ( |
|||
item_id, user_id, group_id, group_weight, name, description |
|||
) VALUES ( |
|||
:item_id, :user_id, :group_id, :group_weight, :name, :description |
|||
) |
|||
`, &item) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *itemRepository) Update(ctx context.Context, item models.Item) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
UPDATE item SET |
|||
group_weight = :group_weight, |
|||
name = :name, |
|||
description = :description |
|||
WHERE item_id=:item_id |
|||
`, &item) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *itemRepository) Delete(ctx context.Context, item models.Item) error { |
|||
_, err := r.db.ExecContext(ctx, `DELETE FROM item WHERE item_id=$1`, item.ID) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return slerrors.NotFound("Item") |
|||
} |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,108 @@ |
|||
package postgres |
|||
|
|||
import ( |
|||
"context" |
|||
"database/sql" |
|||
"github.com/Masterminds/squirrel" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/jmoiron/sqlx" |
|||
) |
|||
|
|||
type logRepository struct { |
|||
db *sqlx.DB |
|||
} |
|||
|
|||
func (r *logRepository) Find(ctx context.Context, id string) (*models.Log, error) { |
|||
res := models.Log{} |
|||
err := r.db.GetContext(ctx, &res, "SELECT * FROM log WHERE log_id=$1", id) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return nil, slerrors.NotFound("Log") |
|||
} |
|||
|
|||
return nil, err |
|||
} |
|||
|
|||
return &res, nil |
|||
} |
|||
|
|||
func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) { |
|||
sq := squirrel.Select("log.*").From("log").PlaceholderFormat(squirrel.Dollar) |
|||
sq = sq.Where(squirrel.Eq{"user_id": filter.UserID}) |
|||
if len(filter.IDs) > 0 { |
|||
sq = sq.Where(squirrel.Eq{"task_id": filter.IDs}) |
|||
} |
|||
if len(filter.ItemIDs) > 0 { |
|||
sq = sq.Where(squirrel.Eq{"item_id": filter.ItemIDs}) |
|||
} |
|||
if filter.MinTime != nil { |
|||
sq = sq.Where(squirrel.GtOrEq{ |
|||
"logged_time": *filter.MinTime, |
|||
}) |
|||
} |
|||
if filter.MaxTime != nil { |
|||
sq = sq.Where(squirrel.LtOrEq{ |
|||
"logged_time": *filter.MaxTime, |
|||
}) |
|||
} |
|||
|
|||
sq = sq.OrderBy("logged_time") |
|||
|
|||
query, args, err := sq.ToSql() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
res := make([]*models.Log, 0, 8) |
|||
err = r.db.SelectContext(ctx, &res, query, args...) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return res, nil |
|||
} |
|||
return nil, err |
|||
} |
|||
|
|||
return res, nil |
|||
} |
|||
|
|||
func (r *logRepository) Insert(ctx context.Context, log models.Log) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
INSERT INTO log ( |
|||
log_id, user_id, task_id, item_id, logged_time, description |
|||
) VALUES ( |
|||
:log_id, :user_id, :task_id, :item_id, :logged_time, :description |
|||
) |
|||
`, &log) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *logRepository) Update(ctx context.Context, log models.Log) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
UPDATE log SET |
|||
logged_time=:logged_time, |
|||
description=:description |
|||
WHERE log_id=:log_id |
|||
`, &log) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *logRepository) Delete(ctx context.Context, log models.Log) error { |
|||
_, err := r.db.ExecContext(ctx, `DELETE FROM log WHERE log_id=$1`, log.ID) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return slerrors.NotFound("Log") |
|||
} |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,104 @@ |
|||
package postgres |
|||
|
|||
import ( |
|||
"context" |
|||
"database/sql" |
|||
"github.com/Masterminds/squirrel" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/jmoiron/sqlx" |
|||
) |
|||
|
|||
type projectRepository struct { |
|||
db *sqlx.DB |
|||
} |
|||
|
|||
func (r *projectRepository) Find(ctx context.Context, id string) (*models.Project, error) { |
|||
res := models.Project{} |
|||
err := r.db.GetContext(ctx, &res, "SELECT * FROM project WHERE project_id=$1", id) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return nil, slerrors.NotFound("Log") |
|||
} |
|||
|
|||
return nil, err |
|||
} |
|||
|
|||
return &res, nil |
|||
} |
|||
|
|||
func (r *projectRepository) List(ctx context.Context, filter models.ProjectFilter) ([]*models.Project, error) { |
|||
sq := squirrel.Select("*").From("project").PlaceholderFormat(squirrel.Dollar) |
|||
sq = sq.Where(squirrel.Eq{"user_id": filter.UserID}) |
|||
if filter.IDs != nil { |
|||
sq = sq.Where(squirrel.Eq{"project_id": filter.IDs}) |
|||
} |
|||
if filter.Active != nil { |
|||
sq = sq.Where(squirrel.Eq{"active": *filter.Active}) |
|||
} |
|||
if filter.Expiring { |
|||
sq = sq.Where("end_time IS NOT NULL") |
|||
} |
|||
|
|||
sq = sq.OrderBy("created_time DESC") |
|||
|
|||
query, args, err := sq.ToSql() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
res := make([]*models.Project, 0, 8) |
|||
err = r.db.SelectContext(ctx, &res, query, args...) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return res, nil |
|||
} |
|||
return nil, err |
|||
} |
|||
|
|||
return res, nil |
|||
} |
|||
|
|||
func (r *projectRepository) Insert(ctx context.Context, project models.Project) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
INSERT INTO project( |
|||
project_id, user_id, name, description, icon, active, created_time, end_time |
|||
) VALUES ( |
|||
:project_id, :user_id, :name, :description, :icon, :active, :created_time, :end_time |
|||
) |
|||
`, &project) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *projectRepository) Update(ctx context.Context, project models.Project) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
UPDATE project SET |
|||
name = :name, |
|||
description = :description, |
|||
icon = :icon, |
|||
active = :active, |
|||
end_time = :end_time |
|||
WHERE project_id=:project_id |
|||
`, &project) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *projectRepository) Delete(ctx context.Context, project models.Project) error { |
|||
_, err := r.db.ExecContext(ctx, `DELETE FROM project WHERE project_id=$1`, project.ID) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return slerrors.NotFound("Project") |
|||
} |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,109 @@ |
|||
package postgres |
|||
|
|||
import ( |
|||
"context" |
|||
"database/sql" |
|||
"github.com/Masterminds/squirrel" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"github.com/jmoiron/sqlx" |
|||
) |
|||
|
|||
type taskRepository struct { |
|||
db *sqlx.DB |
|||
} |
|||
|
|||
func (r *taskRepository) Find(ctx context.Context, id string) (*models.Task, error) { |
|||
res := models.Task{} |
|||
err := r.db.GetContext(ctx, &res, "SELECT task.*, p.icon FROM task INNER JOIN project AS p ON task.project_id = p.project_id WHERE task_id=$1", id) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return nil, slerrors.NotFound("Task") |
|||
} |
|||
|
|||
return nil, err |
|||
} |
|||
|
|||
return &res, nil |
|||
} |
|||
|
|||
func (r *taskRepository) List(ctx context.Context, filter models.TaskFilter) ([]*models.Task, error) { |
|||
sq := squirrel.Select("task.*", "p.icon").From("task").PlaceholderFormat(squirrel.Dollar) |
|||
sq = sq.Where(squirrel.Eq{"task.user_id": filter.UserID}) |
|||
if filter.Active != nil { |
|||
sq = sq.Where(squirrel.Eq{"task.active": *filter.Active}) |
|||
} |
|||
if filter.IDs != nil { |
|||
sq = sq.Where(squirrel.Eq{"task.task_id": filter.IDs}) |
|||
} |
|||
if filter.ItemIDs != nil { |
|||
sq = sq.Where(squirrel.Eq{"task.item_id": filter.ItemIDs}) |
|||
} |
|||
if filter.ProjectIDs != nil { |
|||
sq = sq.Where(squirrel.Eq{"task.project_id": filter.ProjectIDs}) |
|||
} |
|||
|
|||
sq = sq.InnerJoin("project AS p ON task.project_id = p.project_id") |
|||
sq = sq.OrderBy("created_time") |
|||
|
|||
query, args, err := sq.ToSql() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
res := make([]*models.Task, 0, 8) |
|||
err = r.db.SelectContext(ctx, &res, query, args...) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return res, nil |
|||
} |
|||
return nil, err |
|||
} |
|||
|
|||
return res, nil |
|||
} |
|||
|
|||
func (r *taskRepository) Insert(ctx context.Context, task models.Task) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
INSERT INTO task ( |
|||
task_id, user_id, item_id, project_id, item_amount, name, description, active, created_time, end_time |
|||
) VALUES ( |
|||
:task_id, :user_id, :item_id, :project_id, :item_amount, :name, :description, :active, :created_time, :end_time |
|||
) |
|||
`, &task) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *taskRepository) Update(ctx context.Context, task models.Task) error { |
|||
_, err := r.db.NamedExecContext(ctx, ` |
|||
UPDATE task SET |
|||
item_id = :item_id, |
|||
item_amount = :item_amount, |
|||
name = :name, |
|||
description = :description, |
|||
active = :active, |
|||
end_time = :end_time |
|||
WHERE task_id=:task_id |
|||
`, &task) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (r *taskRepository) Delete(ctx context.Context, task models.Task) error { |
|||
_, err := r.db.ExecContext(ctx, `DELETE FROM task WHERE task_id=$1`, task.ID) |
|||
if err != nil { |
|||
if err == sql.ErrNoRows { |
|||
return slerrors.NotFound("Task") |
|||
} |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
@ -0,0 +1,20 @@ |
|||
module github.com/gissleh/stufflog |
|||
|
|||
go 1.14 |
|||
|
|||
require ( |
|||
github.com/AchievementNetwork/stringset v1.1.0 |
|||
github.com/Masterminds/squirrel v1.5.0 |
|||
github.com/aws/aws-lambda-go v1.22.0 |
|||
github.com/awslabs/aws-lambda-go-api-proxy v0.9.0 |
|||
github.com/gin-gonic/gin v1.6.3 |
|||
github.com/go-sql-driver/mysql v1.5.0 // indirect |
|||
github.com/jmoiron/sqlx v1.2.0 |
|||
github.com/lib/pq v1.9.0 |
|||
github.com/mattn/go-sqlite3 v1.14.6 // indirect |
|||
github.com/pkg/errors v0.9.1 // indirect |
|||
github.com/pressly/goose v2.6.0+incompatible // indirect |
|||
github.com/ziutek/mymysql v1.5.4 // indirect |
|||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a |
|||
google.golang.org/appengine v1.6.7 // indirect |
|||
) |
@ -0,0 +1,333 @@ |
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= |
|||
github.com/AchievementNetwork/stringset v1.1.0 h1:6iGjAlend+mCls1tvEAxOBAUTb6nO6QG1/gfBzmPn+s= |
|||
github.com/AchievementNetwork/stringset v1.1.0/go.mod h1:FhOLOVB2mo9zbOR/N+hekGXEnh1VRbJprLlBk/HY71M= |
|||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= |
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= |
|||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= |
|||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= |
|||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= |
|||
github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8= |
|||
github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= |
|||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= |
|||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= |
|||
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= |
|||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= |
|||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= |
|||
github.com/aws/aws-lambda-go v1.19.1/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= |
|||
github.com/aws/aws-lambda-go v1.22.0 h1:X7BKqIdfoJcbsEIi+Lrt5YjX1HnZexIbNWOQgkYKgfE= |
|||
github.com/aws/aws-lambda-go v1.22.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= |
|||
github.com/awslabs/aws-lambda-go-api-proxy v0.9.0 h1:oawiEVOu1ER3ROpDg8CaQ+V7A52frLGD3taPQjTywng= |
|||
github.com/awslabs/aws-lambda-go-api-proxy v0.9.0/go.mod h1:O8jHVv+ga5Kpg8+6i8qSZFp9rnxC1KB/R2yNFNgtFis= |
|||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= |
|||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= |
|||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= |
|||
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE= |
|||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= |
|||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= |
|||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= |
|||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= |
|||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= |
|||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= |
|||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= |
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= |
|||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= |
|||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= |
|||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= |
|||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= |
|||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= |
|||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= |
|||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= |
|||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= |
|||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= |
|||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= |
|||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= |
|||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= |
|||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= |
|||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= |
|||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= |
|||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= |
|||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= |
|||
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= |
|||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= |
|||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= |
|||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= |
|||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= |
|||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= |
|||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= |
|||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= |
|||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= |
|||
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o= |
|||
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= |
|||
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= |
|||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= |
|||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= |
|||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= |
|||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= |
|||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= |
|||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= |
|||
github.com/gofiber/fiber/v2 v2.1.0/go.mod h1:aG+lMkwy3LyVit4CnmYUbUdgjpc3UYOltvlJZ78rgQ0= |
|||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= |
|||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= |
|||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
|||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
|||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
|||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= |
|||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= |
|||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= |
|||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= |
|||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= |
|||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= |
|||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= |
|||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= |
|||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= |
|||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= |
|||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= |
|||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= |
|||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
|||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
|||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
|||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
|||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= |
|||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= |
|||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= |
|||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= |
|||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= |
|||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= |
|||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= |
|||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= |
|||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= |
|||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= |
|||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= |
|||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= |
|||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= |
|||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= |
|||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= |
|||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= |
|||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= |
|||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= |
|||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= |
|||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= |
|||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= |
|||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= |
|||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= |
|||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= |
|||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= |
|||
github.com/kataras/golog v0.0.18/go.mod h1:jRYl7dFYqP8aQj9VkwdBUXYZSfUktm+YYg1arJILfyw= |
|||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= |
|||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= |
|||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= |
|||
github.com/kataras/pio v0.0.8/go.mod h1:NFfMp2kVP1rmV4N6gH6qgWpuoDKlrOeYi3VrAIWCGsE= |
|||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= |
|||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= |
|||
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= |
|||
github.com/klauspost/compress v1.11.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= |
|||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= |
|||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
|||
github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ= |
|||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= |
|||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= |
|||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= |
|||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= |
|||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= |
|||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= |
|||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= |
|||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= |
|||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= |
|||
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= |
|||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= |
|||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= |
|||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= |
|||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= |
|||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= |
|||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= |
|||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= |
|||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= |
|||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= |
|||
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= |
|||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= |
|||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= |
|||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= |
|||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= |
|||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= |
|||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= |
|||
github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w= |
|||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= |
|||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= |
|||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= |
|||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= |
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= |
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= |
|||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= |
|||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= |
|||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= |
|||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= |
|||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= |
|||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= |
|||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= |
|||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= |
|||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= |
|||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= |
|||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
|||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= |
|||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= |
|||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= |
|||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= |
|||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= |
|||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= |
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
|||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= |
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|||
github.com/pressly/goose v2.6.0+incompatible h1:3f8zIQ8rfgP9tyI0Hmcs2YNAqUCL1c+diLe3iU8Qd/k= |
|||
github.com/pressly/goose v2.6.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8= |
|||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= |
|||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= |
|||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= |
|||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= |
|||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= |
|||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= |
|||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= |
|||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= |
|||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= |
|||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= |
|||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= |
|||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= |
|||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= |
|||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= |
|||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= |
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
|||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= |
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
|||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= |
|||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
|||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
|||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= |
|||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= |
|||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= |
|||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= |
|||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= |
|||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= |
|||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= |
|||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= |
|||
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= |
|||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= |
|||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= |
|||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= |
|||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= |
|||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= |
|||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= |
|||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= |
|||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= |
|||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= |
|||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= |
|||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= |
|||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= |
|||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= |
|||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= |
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
|||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
|||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
|||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
|||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= |
|||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= |
|||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= |
|||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= |
|||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
|||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
|||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
|||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
|||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
|||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= |
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= |
|||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= |
|||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= |
|||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= |
|||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= |
|||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= |
|||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20201016160150-f659759dc4ca h1:mLWBs1i4Qi5cHWGEtn2jieJQ2qtwV/gT0A2zLrmzaoE= |
|||
golang.org/x/sys v0.0.0-20201016160150-f659759dc4ca/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
|||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= |
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
|||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
|||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
|||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
|||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= |
|||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
|||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
|||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
|||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= |
|||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= |
|||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= |
|||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= |
|||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= |
|||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= |
|||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= |
|||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= |
|||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= |
|||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= |
|||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= |
|||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= |
|||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= |
|||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= |
|||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= |
|||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= |
|||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= |
|||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= |
|||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= |
|||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= |
|||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= |
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= |
|||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= |
|||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= |
|||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= |
|||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= |
|||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= |
|||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= |
|||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= |
@ -0,0 +1,71 @@ |
|||
package auth |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/base64" |
|||
"encoding/json" |
|||
"github.com/gin-gonic/gin" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"net/http" |
|||
"strings" |
|||
) |
|||
|
|||
var contextKey = struct{}{} |
|||
|
|||
func UserID(ctx context.Context) string { |
|||
if c, ok := ctx.(*gin.Context); ok { |
|||
return UserID(c.Request.Context()) |
|||
} |
|||
|
|||
return ctx.Value(&contextKey).(string) |
|||
} |
|||
|
|||
func DummyMiddleware(uuid string) gin.HandlerFunc { |
|||
return func(c *gin.Context) { |
|||
c.Request = c.Request.WithContext( |
|||
context.WithValue(c.Request.Context(), &contextKey, uuid), |
|||
) |
|||
} |
|||
} |
|||
|
|||
func abortRequest(c *gin.Context) { |
|||
c.AbortWithStatusJSON(http.StatusUnauthorized, slerrors.ErrorResponse{ |
|||
Code: http.StatusUnauthorized, |
|||
Message: "You're not supposed to be here!", |
|||
}) |
|||
} |
|||
|
|||
// TrustingJwtParserMiddleware is meant to be put behind an AWS API gateway that has already
|
|||
// verified this token.
|
|||
func TrustingJwtParserMiddleware() gin.HandlerFunc { |
|||
return func(c *gin.Context) { |
|||
auth := c.GetHeader("Authorization") |
|||
split := strings.Split(auth, ".") |
|||
|
|||
if len(split) >= 3 { |
|||
data, err := base64.RawStdEncoding.DecodeString(split[1]) |
|||
if err != nil { |
|||
abortRequest(c) |
|||
return |
|||
} |
|||
|
|||
fields := make(map[string]interface{}) |
|||
err = json.Unmarshal(data, &fields) |
|||
if err != nil { |
|||
abortRequest(c) |
|||
return |
|||
} |
|||
|
|||
if sub, ok := fields["sub"].(string); ok { |
|||
c.Request = c.Request.WithContext( |
|||
context.WithValue(c.Request.Context(), &contextKey, sub), |
|||
) |
|||
} else { |
|||
abortRequest(c) |
|||
return |
|||
} |
|||
} else { |
|||
abortRequest(c) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,52 @@ |
|||
package generate |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"encoding/hex" |
|||
"log" |
|||
"strings" |
|||
) |
|||
|
|||
func id(prefix string, length int) string { |
|||
var id [16]byte |
|||
var buffer [32]byte |
|||
builder := strings.Builder{} |
|||
builder.Grow(length + 31) |
|||
builder.WriteString(prefix) |
|||
|
|||
for builder.Len() < length { |
|||
_, err := rand.Read(id[:]) |
|||
if err != nil { |
|||
log.Panicln("generate.id: failed to use OS random:", err) |
|||
} |
|||
|
|||
hex.Encode(buffer[:], id[:]) |
|||
builder.Write(buffer[:]) |
|||
} |
|||
|
|||
return builder.String()[:length] |
|||
} |
|||
|
|||
func GroupID() string { |
|||
return id("G", 16) |
|||
} |
|||
|
|||
func ItemID() string { |
|||
return id("I", 16) |
|||
} |
|||
|
|||
func ProjectID() string { |
|||
return id("P", 16) |
|||
} |
|||
|
|||
func TaskID() string { |
|||
return id("T", 16) |
|||
} |
|||
|
|||
func LogID() string { |
|||
return id("L", 16) |
|||
} |
|||
|
|||
func GoalID() string { |
|||
return id("A", 16) |
|||
} |
@ -0,0 +1,18 @@ |
|||
package slerrors |
|||
|
|||
type badRequestError struct { |
|||
Text string |
|||
} |
|||
|
|||
func (err *badRequestError) Error() string { |
|||
return "validation failed: " + err.Text |
|||
} |
|||
|
|||
func BadRequest(text string) error { |
|||
return &badRequestError{Text: text} |
|||
} |
|||
|
|||
func IsBadRequest(err error) bool { |
|||
_, ok := err.(*badRequestError) |
|||
return ok |
|||
} |
@ -0,0 +1,22 @@ |
|||
package slerrors |
|||
|
|||
type forbiddenError struct { |
|||
Message string |
|||
} |
|||
|
|||
func (e *forbiddenError) Error() string { |
|||
return e.Message |
|||
} |
|||
|
|||
func Forbidden(message string) error { |
|||
return &forbiddenError{Message: message} |
|||
} |
|||
|
|||
func IsForbidden(err error) bool { |
|||
if err == nil { |
|||
return false |
|||
} |
|||
|
|||
_, ok := err.(*forbiddenError) |
|||
return ok |
|||
} |
@ -0,0 +1,35 @@ |
|||
package slerrors |
|||
|
|||
import ( |
|||
"github.com/gin-gonic/gin" |
|||
"net/http" |
|||
) |
|||
|
|||
type ErrorResponse struct { |
|||
Code int `json:"errorCode"` |
|||
Message string `json:"errorMessage"` |
|||
} |
|||
|
|||
func Respond(c *gin.Context, err error) { |
|||
if IsNotFound(err) { |
|||
c.JSON(http.StatusNotFound, ErrorResponse{ |
|||
Code: http.StatusNotFound, |
|||
Message: err.Error(), |
|||
}) |
|||
} else if IsForbidden(err) { |
|||
c.JSON(http.StatusForbidden, ErrorResponse{ |
|||
Code: http.StatusForbidden, |
|||
Message: err.Error(), |
|||
}) |
|||
} else if IsBadRequest(err) { |
|||
c.JSON(http.StatusBadRequest, ErrorResponse{ |
|||
Code: http.StatusBadRequest, |
|||
Message: err.Error(), |
|||
}) |
|||
} else { |
|||
c.JSON(http.StatusInternalServerError, ErrorResponse{ |
|||
Code: http.StatusInternalServerError, |
|||
Message: err.Error(), |
|||
}) |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
package slerrors |
|||
|
|||
type notFoundError struct { |
|||
Subject string |
|||
} |
|||
|
|||
func (err *notFoundError) Error() string { |
|||
return err.Subject + " not found" |
|||
} |
|||
|
|||
func NotFound(subject string) error { |
|||
return ¬FoundError{Subject: subject} |
|||
} |
|||
|
|||
func IsNotFound(err error) bool { |
|||
_, ok := err.(*notFoundError) |
|||
return ok |
|||
} |
@ -0,0 +1,15 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE TABLE "group" ( |
|||
group_id CHAR(16) PRIMARY KEY, |
|||
user_id CHAR(36) NOT NULL, |
|||
name TEXT NOT NULL, |
|||
icon TEXT NOT NULL, |
|||
description TEXT NOT NULL |
|||
); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP TABLE "group"; |
|||
-- +goose StatementEnd |
@ -0,0 +1,16 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE TABLE item ( |
|||
item_id CHAR(16) PRIMARY KEY, |
|||
user_id CHAR(36) NOT NULL, |
|||
group_id CHAR(16) NOT NULL, |
|||
group_weight INT NOT NULL, |
|||
name TEXT NOT NULL, |
|||
description TEXT NOT NULL |
|||
); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP TABLE item; |
|||
-- +goose StatementEnd |
@ -0,0 +1,18 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE TABLE project ( |
|||
project_id CHAR(16) PRIMARY KEY, |
|||
user_id CHAR(36) NOT NULL, |
|||
name TEXT NOT NULL, |
|||
description TEXT NOT NULL, |
|||
icon TEXT NOT NULL, |
|||
active BOOLEAN NOT NULL, |
|||
created_time TIMESTAMP NOT NULL, |
|||
end_time TIMESTAMP |
|||
); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP TABLE project; |
|||
-- +goose StatementEnd |
@ -0,0 +1,20 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE TABLE task ( |
|||
task_id CHAR(16) PRIMARY KEY, |
|||
user_id CHAR(36) NOT NULL, |
|||
item_id CHAR(16) NOT NULL, |
|||
project_id CHAR(16) NOT NULL, |
|||
item_amount INT NOT NULL, |
|||
name TEXT NOT NULL, |
|||
description TEXT NOT NULL, |
|||
created_time TIMESTAMP NOT NULL, |
|||
active BOOLEAN NOT NULL, |
|||
end_time TIMESTAMP |
|||
); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP TABLE task; |
|||
-- +goose StatementEnd |
@ -0,0 +1,16 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE TABLE log ( |
|||
log_id CHAR(16) PRIMARY KEY, |
|||
user_id CHAR(36) NOT NULL, |
|||
task_id CHAR(16) NOT NULL, |
|||
item_id CHAR(16) NOT NULL, |
|||
logged_time TIMESTAMP NOT NULL, |
|||
description TEXT NOT NULL |
|||
); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP TABLE log; |
|||
-- +goose StatementEnd |
@ -0,0 +1,18 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE TABLE goal ( |
|||
goal_id CHAR(16) PRIMARY KEY, |
|||
user_id CHAR(36) NOT NULL, |
|||
group_id CHAR(16) NOT NULL, |
|||
start_time TIMESTAMP NOT NULL, |
|||
end_time TIMESTAMP NOT NULL, |
|||
amount INTEGER NOT NULL, |
|||
name TEXT NOT NULL, |
|||
description TEXT NOT NULL |
|||
); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP TABLE goal; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX item_group_id on item(group_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX item_group_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX log_task_id on log(task_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX log_task_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX goal_start_time on goal(start_time); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX goal_start_time; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX goal_end_time on goal(end_time); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX goal_end_time; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX group_user_id on "group"(user_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX group_user_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX item_user_id on item(user_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX item_user_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX project_user_id on project(user_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX project_user_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX task_user_id on task(user_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX task_user_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX log_user_id on log(user_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX log_user_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX goal_user_id on goal(user_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX goal_user_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX project_created_time on project(created_time); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX project_created_time; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX task_item_id on task(item_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX task_item_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX log_logged_time on log(logged_time); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX log_logged_time; |
|||
-- +goose StatementEnd |
@ -0,0 +1,9 @@ |
|||
-- +goose Up |
|||
-- +goose StatementBegin |
|||
CREATE INDEX log_item_id on log(item_id); |
|||
-- +goose StatementEnd |
|||
|
|||
-- +goose Down |
|||
-- +goose StatementBegin |
|||
DROP INDEX log_item_id; |
|||
-- +goose StatementEnd |
@ -0,0 +1,73 @@ |
|||
package models |
|||
|
|||
import ( |
|||
"context" |
|||
"time" |
|||
) |
|||
|
|||
type Goal struct { |
|||
ID string `json:"id" db:"goal_id"` |
|||
UserID string `json:"-" db:"user_id"` |
|||
GroupID string `json:"groupId" db:"group_id"` |
|||
StartTime time.Time `json:"startTime" db:"start_time"` |
|||
EndTime time.Time `json:"endTime" db:"end_time"` |
|||
Amount int `json:"amount" db:"amount"` |
|||
Name string `json:"name" db:"name"` |
|||
Description string `json:"description" db:"description"` |
|||
} |
|||
|
|||
func (goal *Goal) Update(update GoalUpdate) { |
|||
if update.Amount != nil { |
|||
goal.Amount = *update.Amount |
|||
} |
|||
if update.StartTime != nil { |
|||
goal.StartTime = *update.StartTime |
|||
} |
|||
if update.EndTime != nil { |
|||
goal.EndTime = *update.EndTime |
|||
} |
|||
if update.Name != nil { |
|||
goal.Name = *update.Name |
|||
} |
|||
if update.Description != nil { |
|||
goal.Description = *update.Description |
|||
} |
|||
} |
|||
|
|||
type GoalUpdate struct { |
|||
StartTime *time.Time `json:"startTime"` |
|||
EndTime *time.Time `json:"endTime"` |
|||
Amount *int `json:"amount"` |
|||
Name *string `json:"name"` |
|||
Description *string `json:"description"` |
|||
} |
|||
|
|||
type GoalResult struct { |
|||
Goal |
|||
Group *Group `json:"group"` |
|||
Items []*GoalResultItem `json:"items"` |
|||
Logs []*LogResult `json:"logs"` |
|||
CompletedAmount int `json:"completedAmount"` |
|||
} |
|||
|
|||
type GoalResultItem struct { |
|||
Item |
|||
CompletedAmount int `json:"completedAmount"` |
|||
} |
|||
|
|||
type GoalFilter struct { |
|||
UserID string |
|||
GroupIDs []string |
|||
IncludesTime *time.Time |
|||
MinTime *time.Time |
|||
MaxTime *time.Time |
|||
IDs []string |
|||
} |
|||
|
|||
type GoalRepository interface { |
|||
Find(ctx context.Context, id string) (*Goal, error) |
|||
List(ctx context.Context, filter GoalFilter) ([]*Goal, error) |
|||
Insert(ctx context.Context, goal Goal) error |
|||
Update(ctx context.Context, goal Goal) error |
|||
Delete(ctx context.Context, goal Goal) error |
|||
} |
@ -0,0 +1,47 @@ |
|||
package models |
|||
|
|||
import "context" |
|||
|
|||
type Group struct { |
|||
ID string `json:"id" db:"group_id"` |
|||
UserID string `json:"-" db:"user_id"` |
|||
Name string `json:"name" db:"name"` |
|||
Icon string `json:"icon" db:"icon"` |
|||
Description string `json:"description" db:"description"` |
|||
} |
|||
|
|||
func (g *Group) Update(update GroupUpdate) { |
|||
if update.Name != nil { |
|||
g.Name = *update.Name |
|||
} |
|||
if update.Icon != nil { |
|||
g.Icon = *update.Icon |
|||
} |
|||
if update.Description != nil { |
|||
g.Description = *update.Description |
|||
} |
|||
} |
|||
|
|||
type GroupUpdate struct { |
|||
Name *string `json:"name"` |
|||
Icon *string `jsoN:"icon"` |
|||
Description *string `json:"description"` |
|||
} |
|||
|
|||
type GroupResult struct { |
|||
Group |
|||
Items []*Item `json:"items"` |
|||
} |
|||
|
|||
type GroupFilter struct { |
|||
UserID string |
|||
IDs []string |
|||
} |
|||
|
|||
type GroupRepository interface { |
|||
Find(ctx context.Context, id string) (*Group, error) |
|||
List(ctx context.Context, filter GroupFilter) ([]*Group, error) |
|||
Insert(ctx context.Context, group Group) error |
|||
Update(ctx context.Context, group Group) error |
|||
Delete(ctx context.Context, group Group) error |
|||
} |
@ -0,0 +1,50 @@ |
|||
package models |
|||
|
|||
import "context" |
|||
|
|||
type Item struct { |
|||
ID string `json:"id" db:"item_id"` |
|||
UserID string `json:"-" db:"user_id"` |
|||
GroupID string `json:"groupId" db:"group_id"` |
|||
GroupWeight int `json:"groupWeight" db:"group_weight"` |
|||
Icon string `json:"icon" db:"icon"` |
|||
Name string `json:"name" db:"name"` |
|||
Description string `json:"description" db:"description"` |
|||
} |
|||
|
|||
func (item *Item) Update(update ItemUpdate) { |
|||
if update.GroupWeight != nil { |
|||
item.GroupWeight = *update.GroupWeight |
|||
} |
|||
if update.Name != nil { |
|||
item.Name = *update.Name |
|||
} |
|||
if update.Description != nil { |
|||
item.Description = *update.Description |
|||
} |
|||
} |
|||
|
|||
type ItemUpdate struct { |
|||
GroupWeight *int `json:"groupWeight"` |
|||
Name *string `json:"name"` |
|||
Description *string `json:"description"` |
|||
} |
|||
|
|||
type ItemResult struct { |
|||
Item |
|||
Group *Group `json:"group"` |
|||
} |
|||
|
|||
type ItemFilter struct { |
|||
UserID string |
|||
IDs []string |
|||
GroupIDs []string |
|||
} |
|||
|
|||
type ItemRepository interface { |
|||
Find(ctx context.Context, id string) (*Item, error) |
|||
List(ctx context.Context, filter ItemFilter) ([]*Item, error) |
|||
Insert(ctx context.Context, item Item) error |
|||
Update(ctx context.Context, item Item) error |
|||
Delete(ctx context.Context, item Item) error |
|||
} |
@ -0,0 +1,50 @@ |
|||
package models |
|||
|
|||
import ( |
|||
"context" |
|||
"time" |
|||
) |
|||
|
|||
type Log struct { |
|||
ID string `json:"id" db:"log_id"` |
|||
UserID string `json:"-" db:"user_id"` |
|||
TaskID string `json:"taskId" db:"task_id"` |
|||
ItemID string `json:"itemId" db:"item_id"` |
|||
LoggedTime time.Time `json:"loggedTime" db:"logged_time"` |
|||
Description string `json:"description" db:"description"` |
|||
} |
|||
|
|||
func (log *Log) Update(update LogUpdate) { |
|||
if update.LoggedTime != nil { |
|||
log.LoggedTime = update.LoggedTime.UTC() |
|||
} |
|||
if update.Description != nil { |
|||
log.Description = *update.Description |
|||
} |
|||
} |
|||
|
|||
type LogUpdate struct { |
|||
LoggedTime *time.Time `json:"loggedTime"` |
|||
Description *string `json:"description"` |
|||
} |
|||
|
|||
type LogResult struct { |
|||
Log |
|||
Task *Task `json:"task"` |
|||
} |
|||
|
|||
type LogFilter struct { |
|||
UserID string |
|||
IDs []string |
|||
ItemIDs []string |
|||
MinTime *time.Time |
|||
MaxTime *time.Time |
|||
} |
|||
|
|||
type LogRepository interface { |
|||
Find(ctx context.Context, id string) (*Log, error) |
|||
List(ctx context.Context, filter LogFilter) ([]*Log, error) |
|||
Insert(ctx context.Context, log Log) error |
|||
Update(ctx context.Context, log Log) error |
|||
Delete(ctx context.Context, log Log) error |
|||
} |
@ -0,0 +1,68 @@ |
|||
package models |
|||
|
|||
import ( |
|||
"context" |
|||
"time" |
|||
) |
|||
|
|||
type Project struct { |
|||
ID string `json:"id" db:"project_id"` |
|||
UserID string `json:"-" db:"user_id"` |
|||
Name string `json:"name" db:"name"` |
|||
Description string `json:"description" db:"description"` |
|||
Icon string `json:"icon" db:"icon"` |
|||
Active bool `json:"active" db:"active"` |
|||
CreatedTime time.Time `json:"createdTime" db:"created_time"` |
|||
EndTime *time.Time `json:"endTime" db:"end_time"` |
|||
} |
|||
|
|||
func (project *Project) Update(update ProjectUpdate) { |
|||
if update.Name != nil { |
|||
project.Name = *update.Name |
|||
} |
|||
if update.Description != nil { |
|||
project.Description = *update.Description |
|||
} |
|||
if update.Icon != nil { |
|||
project.Icon = *update.Icon |
|||
} |
|||
if update.Active != nil { |
|||
project.Active = *update.Active |
|||
} |
|||
if update.EndTime != nil { |
|||
endTimeCopy := update.EndTime.UTC() |
|||
project.EndTime = &endTimeCopy |
|||
} |
|||
if update.ClearEndTime { |
|||
project.EndTime = nil |
|||
} |
|||
} |
|||
|
|||
type ProjectUpdate struct { |
|||
Name *string `json:"name"` |
|||
Description *string `json:"description"` |
|||
Icon *string `json:"icon"` |
|||
Active *bool `json:"active"` |
|||
EndTime *time.Time `json:"endTime"` |
|||
ClearEndTime bool `json:"clearEndTime"` |
|||
} |
|||
|
|||
type ProjectResult struct { |
|||
Project |
|||
Tasks []*TaskResult `json:"tasks"` |
|||
} |
|||
|
|||
type ProjectFilter struct { |
|||
UserID string |
|||
Active *bool |
|||
Expiring bool |
|||
IDs []string |
|||
} |
|||
|
|||
type ProjectRepository interface { |
|||
Find(ctx context.Context, id string) (*Project, error) |
|||
List(ctx context.Context, filter ProjectFilter) ([]*Project, error) |
|||
Insert(ctx context.Context, project Project) error |
|||
Update(ctx context.Context, project Project) error |
|||
Delete(ctx context.Context, project Project) error |
|||
} |
@ -0,0 +1,74 @@ |
|||
package models |
|||
|
|||
import ( |
|||
"context" |
|||
"time" |
|||
) |
|||
|
|||
type Task struct { |
|||
ID string `json:"id" db:"task_id"` |
|||
UserID string `json:"-" db:"user_id"` |
|||
ItemID string `json:"itemId" db:"item_id"` |
|||
ProjectID string `json:"projectId" db:"project_id"` |
|||
ItemAmount int `json:"itemAmount" db:"item_amount"` |
|||
Name string `json:"name" db:"name"` |
|||
Description string `json:"description" db:"description"` |
|||
Icon string `json:"icon" db:"icon"` |
|||
Active bool `json:"active" db:"active"` |
|||
CreatedTime time.Time `json:"createdTime" db:"created_time"` |
|||
EndTime *time.Time `json:"endTime" db:"end_time"` |
|||
} |
|||
|
|||
func (task *Task) Update(update TaskUpdate) { |
|||
if update.ItemAmount != nil { |
|||
task.ItemAmount = *update.ItemAmount |
|||
} |
|||
if update.Name != nil { |
|||
task.Name = *update.Name |
|||
} |
|||
if update.Description != nil { |
|||
task.Description = *update.Description |
|||
} |
|||
if update.Active != nil { |
|||
task.Active = *update.Active |
|||
} |
|||
if update.EndTime != nil { |
|||
endTimeCopy := update.EndTime.UTC() |
|||
task.EndTime = &endTimeCopy |
|||
} |
|||
if update.ClearEndTime { |
|||
task.EndTime = nil |
|||
} |
|||
} |
|||
|
|||
type TaskUpdate struct { |
|||
ItemAmount *int `json:"itemAmount"` |
|||
Name *string `json:"name"` |
|||
Description *string `json:"description"` |
|||
Active *bool `json:"active"` |
|||
EndTime *time.Time `json:"endTime"` |
|||
ClearEndTime bool `json:"clearEndTime"` |
|||
} |
|||
|
|||
type TaskResult struct { |
|||
Task |
|||
Item *Item `json:"item"` |
|||
Logs []*Log `json:"logs"` |
|||
CompletedAmount int `json:"completedAmount"` |
|||
} |
|||
|
|||
type TaskFilter struct { |
|||
UserID string |
|||
Active *bool |
|||
IDs []string |
|||
ItemIDs []string |
|||
ProjectIDs []string |
|||
} |
|||
|
|||
type TaskRepository interface { |
|||
Find(ctx context.Context, id string) (*Task, error) |
|||
List(ctx context.Context, filter TaskFilter) ([]*Task, error) |
|||
Insert(ctx context.Context, task Task) error |
|||
Update(ctx context.Context, task Task) error |
|||
Delete(ctx context.Context, task Task) error |
|||
} |
@ -0,0 +1,115 @@ |
|||
service: stufflog2 |
|||
|
|||
frameworkVersion: '2' |
|||
|
|||
provider: |
|||
name: aws |
|||
runtime: go1.x |
|||
memorySize: 512 |
|||
stage: prod |
|||
region: ${env:AWS_DEFAULT_REGION} |
|||
role: ${env:AMI_ROLE} |
|||
tags: |
|||
Name: Stufflog2 |
|||
Type: Application |
|||
apiGateway: |
|||
shouldStartNameWithService: true |
|||
minimumCompressionSize: 2048 |
|||
versionFunctions: false |
|||
environment: |
|||
DB_DRIVER: ${env:DB_DRIVER} |
|||
DB_CONNECT: ${env:DB_CONNECT} |
|||
|
|||
functions: |
|||
handleApiCalls: |
|||
handler: ./build/api/handler |
|||
package: |
|||
include: |
|||
- ./build/api/handler |
|||
events: |
|||
- http: ANY /api/{proxy+} |
|||
timeout: 30 |
|||
|
|||
package: |
|||
individually: true |
|||
exclude: |
|||
- ./** |
|||
|
|||
plugins: |
|||
- serverless-domain-manager |
|||
- serverless-apigateway-service-proxy |
|||
|
|||
custom: |
|||
webuiBucket: ${env:S3_WEBUI_BUCKET} |
|||
|
|||
apiGatewayServiceProxies: |
|||
- s3: |
|||
path: / |
|||
method: get |
|||
action: GetObject |
|||
bucket: ${self:custom.webuiBucket} |
|||
roleArn: ${self:provider.role} |
|||
key: index.html |
|||
requestParameters: |
|||
'integration.request.header.cache-control': "'public, max-age=86400, immutable'" |
|||
|
|||
- s3: |
|||
path: /goals |
|||
method: get |
|||
action: GetObject |
|||
bucket: ${self:custom.webuiBucket} |
|||
roleArn: ${self:provider.role} |
|||
key: index.html |
|||
requestParameters: |
|||
'integration.request.header.cache-control': "'public, max-age=86400, immutable'" |
|||
|
|||
- s3: |
|||
path: /projects |
|||
method: get |
|||
action: GetObject |
|||
bucket: ${self:custom.webuiBucket} |
|||
roleArn: ${self:provider.role} |
|||
key: index.html |
|||
requestParameters: |
|||
'integration.request.header.cache-control': "'public, max-age=86400, immutable'" |
|||
|
|||
- s3: |
|||
path: /items |
|||
method: get |
|||
action: GetObject |
|||
bucket: ${self:custom.webuiBucket} |
|||
roleArn: ${self:provider.role} |
|||
key: index.html |
|||
requestParameters: |
|||
'integration.request.header.cache-control': "'public, max-age=86400, immutable'" |
|||
|
|||
- s3: |
|||
path: /logs |
|||
method: get |
|||
action: GetObject |
|||
bucket: ${self:custom.webuiBucket} |
|||
roleArn: ${self:provider.role} |
|||
key: index.html |
|||
requestParameters: |
|||
'integration.request.header.cache-control': "'public, max-age=86400, immutable'" |
|||
|
|||
- s3: |
|||
path: /{myPath+} |
|||
method: get |
|||
action: GetObject |
|||
bucket: ${self:custom.webuiBucket} |
|||
roleArn: ${self:provider.role} |
|||
requestParameters: |
|||
'integration.request.path.myPath': 'method.request.path.myPath' |
|||
'integration.request.path.object': 'method.request.path.myPath' |
|||
'integration.request.header.cache-control': "'public, max-age=86400, immutable'" |
|||
|
|||
customDomain: |
|||
domainName: ${env:DOMAIN_NAME} |
|||
basePath: '' |
|||
certificateName: ${env:CERTIFICATE_NAME} |
|||
certificateArn: ${env:CERTIFICATE_ARN} |
|||
createRoute53Record: true |
|||
endpointType: 'regional' |
|||
hostedZoneId: ${env:HOSTED_ZONE_ID} |
|||
autoDomain: true |
@ -0,0 +1,508 @@ |
|||
package services |
|||
|
|||
import ( |
|||
"context" |
|||
"github.com/AchievementNetwork/stringset" |
|||
"github.com/gissleh/stufflog/database" |
|||
"github.com/gissleh/stufflog/internal/auth" |
|||
"github.com/gissleh/stufflog/internal/slerrors" |
|||
"github.com/gissleh/stufflog/models" |
|||
"golang.org/x/sync/errgroup" |
|||
) |
|||
|
|||
// Loader loads the stuff.
|
|||
type Loader struct { |
|||
DB database.Database |
|||
} |
|||
|
|||
func (l *Loader) FindGroup(ctx context.Context, id string) (*models.GroupResult, error) { |
|||
group, err := l.DB.Groups().Find(ctx, id) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if group.UserID != auth.UserID(ctx) { |
|||
return nil, slerrors.NotFound("Goal") |
|||
} |
|||
|
|||
result := &models.GroupResult{Group: *group} |
|||
result.Items, err = l.DB.Items().List(ctx, models.ItemFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
GroupIDs: []string{group.ID}, |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return result, nil |
|||
} |
|||
|
|||
func (l *Loader) ListGroups(ctx context.Context, filter models.GroupFilter) ([]*models.GroupResult, error) { |
|||
filter.UserID = auth.UserID(ctx) |
|||
groups, err := l.DB.Groups().List(ctx, filter) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
groupIDs := make([]string, 0, len(groups)) |
|||
for _, group := range groups { |
|||
groupIDs = append(groupIDs, group.ID) |
|||
} |
|||
items, err := l.DB.Items().List(ctx, models.ItemFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
GroupIDs: groupIDs, |
|||
}) |
|||
|
|||
results := make([]*models.GroupResult, len(groups)) |
|||
for i, group := range groups { |
|||
results[i] = &models.GroupResult{Group: *group, Items: []*models.Item{}} |
|||
for _, item := range items { |
|||
if item.GroupID == group.ID { |
|||
results[i].Items = append(results[i].Items, item) |
|||
} |
|||
} |
|||
} |
|||
|
|||
return results, nil |
|||
} |
|||
|
|||
func (l *Loader) FindItem(ctx context.Context, id string) (*models.ItemResult, error) { |
|||
item, err := l.DB.Items().Find(ctx, id) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if item.UserID != auth.UserID(ctx) { |
|||
return nil, slerrors.NotFound("Item") |
|||
} |
|||
|
|||
result := &models.ItemResult{Item: *item} |
|||
result.Group, err = l.DB.Groups().Find(ctx, item.GroupID) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return result, nil |
|||
} |
|||
|
|||
func (l *Loader) ListItems(ctx context.Context, filter models.ItemFilter) ([]*models.ItemResult, error) { |
|||
filter.UserID = auth.UserID(ctx) |
|||
items, err := l.DB.Items().List(ctx, filter) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
groupIDs := make([]string, 0, len(items)) |
|||
for _, item := range items { |
|||
groupIDs = append(groupIDs, item.GroupID) |
|||
} |
|||
groups, err := l.DB.Groups().List(ctx, models.GroupFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
IDs: groupIDs, |
|||
}) |
|||
|
|||
results := make([]*models.ItemResult, len(items)) |
|||
for i, item := range items { |
|||
results[i] = &models.ItemResult{Item: *item} |
|||
for _, group := range groups { |
|||
if item.GroupID == group.ID { |
|||
results[i].Group = group |
|||
break |
|||
} |
|||
} |
|||
} |
|||
|
|||
return results, nil |
|||
} |
|||
|
|||
func (l *Loader) FindLog(ctx context.Context, id string) (*models.LogResult, error) { |
|||
log, err := l.DB.Logs().Find(ctx, id) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if log.UserID != auth.UserID(ctx) { |
|||
return nil, slerrors.NotFound("Goal") |
|||
} |
|||
result := &models.LogResult{ |
|||
Log: *log, |
|||
Task: nil, |
|||
} |
|||
|
|||
result.Task, _ = l.DB.Tasks().Find(ctx, id) |
|||
|
|||
return result, nil |
|||
} |
|||
|
|||
func (l *Loader) ListLogs(ctx context.Context, filter models.LogFilter) ([]*models.LogResult, error) { |
|||
filter.UserID = auth.UserID(ctx) |
|||
logs, err := l.DB.Logs().List(ctx, filter) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
taskIDs := stringset.New() |
|||
for _, log := range logs { |
|||
taskIDs.Add(log.TaskID) |
|||
} |
|||
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
IDs: taskIDs.Strings(), |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
results := make([]*models.LogResult, len(logs)) |
|||
for i, log := range logs { |
|||
results[i] = &models.LogResult{ |
|||
Log: *log, |
|||
Task: nil, |
|||
} |
|||
|
|||
for _, task := range tasks { |
|||
if task.ID == log.TaskID { |
|||
results[i].Task = task |
|||
break |
|||
} |
|||
} |
|||
} |
|||
|
|||
return results, nil |
|||
} |
|||
|
|||
func (l *Loader) FindProject(ctx context.Context, id string) (*models.ProjectResult, error) { |
|||
project, err := l.DB.Projects().Find(ctx, id) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if project.UserID != auth.UserID(ctx) { |
|||
return nil, slerrors.NotFound("Goal") |
|||
} |
|||
result := &models.ProjectResult{Project: *project} |
|||
|
|||
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
ProjectIDs: []string{project.ID}, |
|||
}) |
|||
taskIDs := make([]string, 0, len(tasks)) |
|||
itemIDs := stringset.New() |
|||
for _, task := range tasks { |
|||
taskIDs = append(taskIDs, task.ID) |
|||
itemIDs.Add(task.ItemID) |
|||
} |
|||
|
|||
logs, err := l.DB.Logs().List(ctx, models.LogFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
IDs: taskIDs, |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
items, err := l.DB.Items().List(ctx, models.ItemFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
IDs: itemIDs.Strings(), |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
result.Tasks = make([]*models.TaskResult, len(tasks)) |
|||
for i, task := range tasks { |
|||
result.Tasks[i] = &models.TaskResult{ |
|||
Logs: []*models.Log{}, |
|||
} |
|||
result.Tasks[i].Task = *task |
|||
for _, log := range logs { |
|||
if log.TaskID == task.ID { |
|||
result.Tasks[i].Logs = append(result.Tasks[i].Logs, log) |
|||
} |
|||
} |
|||
for _, item := range items { |
|||
if item.ID == task.ItemID { |
|||
result.Tasks[i].Item = item |
|||
break |
|||
} |
|||
} |
|||
result.Tasks[i].CompletedAmount = len(result.Tasks[i].Logs) |
|||
} |
|||
|
|||
return result, nil |
|||
} |
|||
|
|||
func (l *Loader) ListProjects(ctx context.Context, filter models.ProjectFilter) ([]*models.ProjectResult, error) { |
|||
filter.UserID = auth.UserID(ctx) |
|||
projects, err := l.DB.Projects().List(ctx, filter) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
projectIDs := make([]string, 0, len(projects)) |
|||
for _, project := range projects { |
|||
projectIDs = append(projectIDs, project.ID) |
|||
} |
|||
|
|||
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
ProjectIDs: projectIDs, |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
taskIDs := make([]string, 0, len(tasks)) |
|||
itemIDs := stringset.New() |
|||
for _, task := range tasks { |
|||
taskIDs = append(taskIDs, task.ID) |
|||
itemIDs.Add(task.ItemID) |
|||
} |
|||
|
|||
logs, err := l.DB.Logs().List(ctx, models.LogFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
IDs: taskIDs, |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
items, err := l.DB.Items().List(ctx, models.ItemFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
IDs: itemIDs.Strings(), |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
results := make([]*models.ProjectResult, len(projects)) |
|||
for i, project := range projects { |
|||
results[i] = &models.ProjectResult{Project: *project} |
|||
results[i].Tasks = make([]*models.TaskResult, 0, 16) |
|||
for _, task := range tasks { |
|||
if task.ProjectID != project.ID { |
|||
continue |
|||
} |
|||
|
|||
taskResult := &models.TaskResult{ |
|||
Task: *task, |
|||
Logs: []*models.Log{}, |
|||
} |
|||
for _, log := range logs { |
|||
if log.TaskID == task.ID { |
|||
taskResult.Logs = append(taskResult.Logs, log) |
|||
} |
|||
} |
|||
for _, item := range items { |
|||
if item.ID == task.ItemID { |
|||
taskResult.Item = item |
|||
break |
|||
} |
|||
} |
|||
taskResult.CompletedAmount = len(taskResult.Logs) |
|||
|
|||
results[i].Tasks = append(results[i].Tasks, taskResult) |
|||
} |
|||
} |
|||
|
|||
return results, nil |
|||
} |
|||
|
|||
func (l *Loader) FindTask(ctx context.Context, id string) (*models.TaskResult, error) { |
|||
task, err := l.DB.Tasks().Find(ctx, id) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if task.UserID != auth.UserID(ctx) { |
|||
return nil, slerrors.NotFound("Goal") |
|||
} |
|||
result := &models.TaskResult{Task: *task} |
|||
|
|||
result.Item, _ = l.DB.Items().Find(ctx, task.ItemID) |
|||
result.Logs, err = l.DB.Logs().List(ctx, models.LogFilter{ |
|||
UserID: task.UserID, |
|||
IDs: []string{task.ID}, |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
result.CompletedAmount = len(result.Logs) |
|||
|
|||
return result, nil |
|||
} |
|||
|
|||
func (l *Loader) ListTasks(ctx context.Context, filter models.TaskFilter) ([]*models.TaskResult, error) { |
|||
filter.UserID = auth.UserID(ctx) |
|||
tasks, err := l.DB.Tasks().List(ctx, filter) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if len(tasks) == 0 { |
|||
return []*models.TaskResult{}, nil |
|||
} |
|||
|
|||
taskIDs := make([]string, 0, len(tasks)) |
|||
itemIDs := stringset.New() |
|||
for _, task := range tasks { |
|||
taskIDs = append(taskIDs, task.ID) |
|||
itemIDs.Add(task.ItemID) |
|||
} |
|||
|
|||
logs, err := l.DB.Logs().List(ctx, models.LogFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
IDs: taskIDs, |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
items, err := l.DB.Items().List(ctx, models.ItemFilter{ |
|||
UserID: auth.UserID(ctx), |
|||
IDs: itemIDs.Strings(), |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
results := make([]*models.TaskResult, 0, len(tasks)) |
|||
for _, task := range tasks { |
|||
result := &models.TaskResult{ |
|||
Task: *task, |
|||
Logs: []*models.Log{}, |
|||
} |
|||
|
|||
for _, log := range logs { |
|||
if log.TaskID != task.ID { |
|||
result.Logs = append(result.Logs, log) |
|||
} |
|||
} |
|||
|
|||
for _, item := range items { |
|||
if item.ID == task.ItemID { |
|||
result.Item = item |
|||
break |
|||
} |
|||
} |
|||
|
|||
result.CompletedAmount = len(result.Logs) |
|||
|
|||
results = append(results, result) |
|||
} |
|||
|
|||
return results, nil |
|||
} |
|||
|
|||
func (l *Loader) FindGoal(ctx context.Context, id string) (*models.GoalResult, error) { |
|||
goal, err := l.DB.Goals().Find(ctx, id) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if goal.UserID != auth.UserID(ctx) { |
|||
return nil, slerrors.NotFound("Goal") |
|||
} |
|||
|
|||
return l.populateGoals(ctx, goal) |
|||
} |
|||
|
|||
func (l *Loader) ListGoals(ctx context.Context, filter models.GoalFilter) ([]*models.GoalResult, error) { |
|||
filter.UserID = auth.UserID(ctx) |
|||
|
|||
goals, err := l.DB.Goals().List(ctx, filter) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
results := make([]*models.GoalResult, len(goals)) |
|||
eg := errgroup.Group{} |
|||
for i := range results { |
|||
index := i // Required to avoid race condition.
|
|||
|
|||
eg.Go(func() error { |
|||
res, err := l.populateGoals(ctx, goals[index]) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
results[index] = res |
|||
|
|||
return nil |
|||
}) |
|||
} |
|||
err = eg.Wait() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return results, nil |
|||
} |
|||
|
|||
func (l *Loader) populateGoals(ctx context.Context, goal *models.Goal) (*models.GoalResult, error) { |
|||
userID := auth.UserID(ctx) |
|||
result := &models.GoalResult{ |
|||
Goal: *goal, |
|||
Group: nil, |
|||
Items: nil, |
|||
Logs: nil, |
|||
CompletedAmount: 0, |
|||
} |
|||
|
|||
result.Group, _ = l.DB.Groups().Find(ctx, goal.GroupID) |
|||
if result.Group != nil { |
|||
// Get items
|
|||
items, err := l.DB.Items().List(ctx, models.ItemFilter{ |
|||
UserID: userID, |
|||
GroupIDs: []string{goal.GroupID}, |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
itemIDs := make([]string, 0, len(items)) |
|||
for _, item := range items { |
|||
result.Items = append(result.Items, &models.GoalResultItem{ |
|||
Item: *item, |
|||
CompletedAmount: 0, |
|||
}) |
|||
|
|||
itemIDs = append(itemIDs, item.ID) |
|||
} |
|||
|
|||
// Get logs
|
|||
logs, err := l.DB.Logs().List(ctx, models.LogFilter{ |
|||
UserID: userID, |
|||
ItemIDs: itemIDs, |
|||
MinTime: &goal.StartTime, |
|||
MaxTime: &goal.EndTime, |
|||
}) |
|||
|
|||
// Get tasks
|
|||
taskIDs := make([]string, 0, len(result.Logs)) |
|||
for _, log := range logs { |
|||
taskIDs = append(taskIDs, log.TaskID) |
|||
} |
|||
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{ |
|||
UserID: userID, |
|||
IDs: taskIDs, |
|||
}) |
|||
|
|||
// Apply logs
|
|||
result.Logs = make([]*models.LogResult, 0, len(logs)) |
|||
for _, log := range logs { |
|||
resultLog := &models.LogResult{ |
|||
Log: *log, |
|||
} |
|||
|
|||
for _, task := range tasks { |
|||
if task.ID == log.TaskID { |
|||
resultLog.Task = task |
|||
|
|||
for _, item := range result.Items { |
|||
if task.ItemID == item.ID { |
|||
item.CompletedAmount += 1 |
|||
result.CompletedAmount += item.GroupWeight |
|||
break |
|||
} |
|||
} |
|||
|
|||
break |
|||
} |
|||
} |
|||
result.Logs = append(result.Logs, resultLog) |
|||
} |
|||
} |
|||
|
|||
return result, nil |
|||
} |
@ -0,0 +1,7 @@ |
|||
/node_modules/ |
|||
/public/build/ |
|||
/build.env |
|||
/.idea |
|||
/.vscode |
|||
|
|||
.DS_Store |
4368
svelte-ui/package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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" |
|||
} |
|||
} |
After Width: 128 | Height: 128 | Size: 3.1 KiB |
@ -0,0 +1,62 @@ |
|||
html, body { |
|||
position: relative; |
|||
width: 100%; |
|||
height: 100%; |
|||
margin: 0; |
|||
padding: 0; |
|||
} |
|||
|
|||
body { |
|||
background-color: #111; |
|||
color: #CCC; |
|||
margin: 0; |
|||
padding: 8px; |
|||
box-sizing: border-box; |
|||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; |
|||
} |
|||
|
|||
a { |
|||
color: #FC1; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
a:hover { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
label { |
|||
display: block; |
|||
} |
|||
|
|||
input, button, select, textarea { |
|||
font-family: inherit; |
|||
font-size: inherit; |
|||
-webkit-padding: 0.4em 0; |
|||
padding: 0.4em; |
|||
margin: 0 0 0.5em 0; |
|||
box-sizing: border-box; |
|||
border: 1px solid #ccc; |
|||
border-radius: 2px; |
|||
} |
|||
|
|||
input:disabled { |
|||
color: #ccc; |
|||
} |
|||
|
|||
button { |
|||
color: #333; |
|||
background-color: #f4f4f4; |
|||
outline: none; |
|||
} |
|||
|
|||
button:disabled { |
|||
color: #999; |
|||
} |
|||
|
|||
button:not(:disabled):active { |
|||
background-color: #ddd; |
|||
} |
|||
|
|||
button:focus { |
|||
border-color: #666; |
|||
} |
@ -0,0 +1,17 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset='utf-8'> |
|||
<meta name='viewport' content='width=device-width,initial-scale=1'> |
|||
|
|||
<title>Svelte app</title> |
|||
|
|||
<link rel='icon' type='image/png' href='/favicon.png'> |
|||
<link rel='stylesheet' href='/global.css'> |
|||
<link rel='stylesheet' href='/build/bundle.css'> |
|||
|
|||
<script defer src='/build/bundle.js'></script> |
|||
</head> |
|||
|
|||
<body></body> |
|||
</html> |
@ -0,0 +1,99 @@ |
|||
import fs from "fs"; |
|||
|
|||
import svelte from "rollup-plugin-svelte"; |
|||
import commonjs from "@rollup/plugin-commonjs"; |
|||
import resolve from "@rollup/plugin-node-resolve"; |
|||
import livereload from "rollup-plugin-livereload"; |
|||
import { terser } from "rollup-plugin-terser"; |
|||
import sveltePreprocess from "svelte-preprocess"; |
|||
import typescript from "@rollup/plugin-typescript"; |
|||
import css from "rollup-plugin-css-only"; |
|||
import dev from "rollup-plugin-dev"; |
|||
import replace from "@rollup/plugin-replace"; |
|||
import json from "@rollup/plugin-json"; |
|||
|
|||
const production = !process.env.ROLLUP_WATCH; |
|||
const envVariables = fs.readFileSync("build.env", "utf-8") |
|||
.split("\n") |
|||
.filter(l => l.length > 0) |
|||
.map(l => l.trim().split("=")) |
|||
.reduce((p, [key, value]) => ({...p, [key]: value}), {}); |
|||
|
|||
export default { |
|||
input: "src/main.ts", |
|||
output: { |
|||
sourcemap: true, |
|||
format: "iife", |
|||
name: "app", |
|||
file: "public/build/bundle.js" |
|||
}, |
|||
plugins: [ |
|||
svelte({ |
|||
preprocess: sveltePreprocess(), |
|||
compilerOptions: { |
|||
// enable run-time checks when not in production
|
|||
dev: !production |
|||
} |
|||
}), |
|||
// we"ll extract any component CSS out into
|
|||
// a separate file - better for performance
|
|||
css({ output: "bundle.css" }), |
|||
|
|||
// If you have external dependencies installed from
|
|||
// npm, you'll most likely need these plugins. In
|
|||
// some cases you'll need additional configuration -
|
|||
// consult the documentation for details:
|
|||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
|||
resolve({ |
|||
browser: true, |
|||
preferBuiltins: false, |
|||
dedupe: ["svelte"], |
|||
}), |
|||
json(), |
|||
commonjs({ |
|||
include: 'node_modules/**', |
|||
}), |
|||
typescript({ |
|||
sourceMap: !production, |
|||
inlineSources: !production |
|||
}), |
|||
|
|||
replace({ |
|||
// 2 level deep object should be stringify
|
|||
"process.env": JSON.stringify({ |
|||
NODE_ENV: production ? "production" : "development", |
|||
...envVariables, |
|||
}), |
|||
}), |
|||
|
|||
// Watch the `public` directory and refresh the
|
|||
// browser on changes when not in production
|
|||
!production && livereload("public"), |
|||
|
|||
// Add dev server in development.
|
|||
!production && dev({ |
|||
dirs: ["public"], |
|||
spa: "public/index.html", |
|||
port: 5000, |
|||
proxy: { |
|||
"/api/*": "localhost:8000", |
|||
}, |
|||
}), |
|||
|
|||
// If we're building for production (npm run build
|
|||
// instead of npm run dev), minify
|
|||
production && terser({ |
|||
output: { comments: false }, |
|||
}) |
|||
], |
|||
watch: { |
|||
clearScreen: false |
|||
}, |
|||
onwarn: function(warning) { |
|||
// Skip certain warnings
|
|||
if ( warning.code === "THIS_IS_UNDEFINED" ) { return; } |
|||
|
|||
// console.warn everything else
|
|||
console.warn( warning.message ); |
|||
} |
|||
}; |
@ -0,0 +1,77 @@ |
|||
<script lang="ts"> |
|||
import { Router, Link, Route } from "svelte-routing"; |
|||
import { onMount } from "svelte"; |
|||
import Menu from "./components/Menu.svelte"; |
|||
import FrontPage from "./pages/FrontPage.svelte"; |
|||
import ProjectPage from "./pages/ProjectPage.svelte"; |
|||
import ModalRoute from "./components/ModalRoute.svelte"; |
|||
import LogAddForm from "./forms/LogAddForm.svelte"; |
|||
import LogsPage from "./pages/LogsPage.svelte"; |
|||
import LogEditForm from "./forms/LogEditForm.svelte"; |
|||
import LogDeleteForm from "./forms/LogDeleteForm.svelte"; |
|||
import TaskAddForm from "./forms/TaskAddForm.svelte"; |
|||
import TaskEditForm from "./forms/TaskEditForm.svelte"; |
|||
import TaskDeleteForm from "./forms/TaskDeleteForm.svelte"; |
|||
import ProjectAddForm from "./forms/ProjectAddForm.svelte"; |
|||
import ProjectEditForm from "./forms/ProjectEditForm.svelte"; |
|||
import ProjectDeleteForm from "./forms/ProjectDeleteForm.svelte"; |
|||
import GroupPage from "./pages/GroupPage.svelte"; |
|||
import ItemAddForm from "./forms/ItemAddForm.svelte"; |
|||
import ItemEditForm from "./forms/ItemEditForm.svelte"; |
|||
import ItemDeleteForm from "./forms/ItemDeleteForm.svelte"; |
|||
import GroupForm from "./forms/GroupForm.svelte"; |
|||
import GoalPage from "./pages/GoalPage.svelte"; |
|||
import GoalForm from "./forms/GoalForm.svelte"; |
|||
import LoginForm from "./forms/LoginForm.svelte"; |
|||
import authStore from "./stores/auth"; |
|||
|
|||
onMount(() => { |
|||
authStore.check() |
|||
}); |
|||
</script> |
|||
|
|||
{#if $authStore.checked} |
|||
{#if $authStore.loggedIn} |
|||
<Router> |
|||
<Menu /> |
|||
<main> |
|||
<Route path="/" component={FrontPage} /> |
|||
<Route path="/goals/" component={GoalPage} /> |
|||
<Route path="/projects/" component={ProjectPage} /> |
|||
<Route path="/logs/" component={LogsPage} /> |
|||
<Route path="/items/" component={GroupPage} /> |
|||
</main> |
|||
</Router> |
|||
<ModalRoute name="log.add"> <LogAddForm/> </ModalRoute> |
|||
<ModalRoute name="log.edit"> <LogEditForm/> </ModalRoute> |
|||
<ModalRoute name="log.delete"> <LogDeleteForm/> </ModalRoute> |
|||
<ModalRoute name="task.add"> <TaskAddForm/> </ModalRoute> |
|||
<ModalRoute name="task.edit"> <TaskEditForm/> </ModalRoute> |
|||
<ModalRoute name="task.delete"> <TaskDeleteForm/> </ModalRoute> |
|||
<ModalRoute name="project.add"> <ProjectAddForm/> </ModalRoute> |
|||
<ModalRoute name="project.edit"> <ProjectEditForm/> </ModalRoute> |
|||
<ModalRoute name="project.delete"> <ProjectDeleteForm/> </ModalRoute> |
|||
<ModalRoute name="item.add"> <ItemAddForm/> </ModalRoute> |
|||
<ModalRoute name="item.edit"> <ItemEditForm/> </ModalRoute> |
|||
<ModalRoute name="item.delete"> <ItemDeleteForm/> </ModalRoute> |
|||
<ModalRoute name="group.add"> <GroupForm creation/> </ModalRoute> |
|||
<ModalRoute name="group.edit"> <GroupForm/> </ModalRoute> |
|||
<ModalRoute name="group.delete"> <GroupForm deletion/> </ModalRoute> |
|||
<ModalRoute name="goal.add"> <GoalForm creation/> </ModalRoute> |
|||
<ModalRoute name="goal.edit"> <GoalForm/> </ModalRoute> |
|||
<ModalRoute name="goal.delete"> <GoalForm deletion/> </ModalRoute> |
|||
{:else} |
|||
<LoginForm /> |
|||
{/if} |
|||
{/if} |
|||
|
|||
<style> |
|||
main { |
|||
text-align: left; |
|||
max-width: 99.5%; |
|||
width: 920px; |
|||
margin: 1em auto; |
|||
padding-bottom: 4em; |
|||
} |
|||
|
|||
</style> |
@ -0,0 +1,46 @@ |
|||
import Amplify from "@aws-amplify/core"; |
|||
import Auth from "@aws-amplify/auth"; |
|||
import type {CognitoAccessToken, CognitoUser} from "amazon-cognito-identity-js"; |
|||
|
|||
Amplify.configure({ |
|||
Auth: { |
|||
region: process.env.AWS_AMPLIFY_REGION, |
|||
userPoolId: process.env.AWS_AMPLIFY_USER_POOL_ID, |
|||
userPoolWebClientId: process.env.AWS_AMPLIFY_USER_POOL_WEB_CLIENT_ID, |
|||
}, |
|||
}); |
|||
|
|||
export async function signIn(username: string, password: string): Promise<CognitoUser | null> { |
|||
const u = await Auth.signIn(username, password); |
|||
return u || null; |
|||
} |
|||
|
|||
export async function signOut(): Promise<void> { |
|||
await Auth.signOut(); |
|||
} |
|||
|
|||
async function getAccessToken(): Promise<CognitoAccessToken | null> { |
|||
try { |
|||
const u = await Auth.currentSession(); |
|||
if (!u || !u.isValid()) { |
|||
return null; |
|||
} |
|||
|
|||
return u.getAccessToken(); |
|||
} catch (e) { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
export async function getJwt(): Promise<string> { |
|||
const token = await getAccessToken(); |
|||
if (!token) { |
|||
throw new Error("unauthorized"); |
|||
} |
|||
|
|||
return token.getJwtToken(); |
|||
} |
|||
|
|||
export async function checkSession(): Promise<boolean> { |
|||
return !!(await getAccessToken()); |
|||
} |
@ -0,0 +1,260 @@ |
|||
import { getJwt } from "./amplify"; |
|||
import type { GoalFilter, GoalInput, GoalResult, GoalUpdate } from "../models/goal"; |
|||
import type { ProjectFilter, ProjectInput, ProjectResult, ProjectUpdate } from "../models/project"; |
|||
import type { TaskInput, TaskResult, TaskUpdate } from "../models/task"; |
|||
import type { LogFilter, LogInput, LogResult, LogUpdate } from "../models/log"; |
|||
import type { GroupInput, GroupResult, GroupUpdate } from "../models/group"; |
|||
import type { ItemInput, ItemResult, ItemUpdate } from "../models/item"; |
|||
|
|||
export class StufflogClient { |
|||
private root: string; |
|||
|
|||
constructor(root: string) { |
|||
this.root = root; |
|||
} |
|||
|
|||
|
|||
|
|||
async findGoal(id: string): Promise<GoalResult> { |
|||
const data = await this.fetch("GET", `/api/goal/${id}`); |
|||
return data.goal; |
|||
} |
|||
|
|||
async listGoals({minTime, maxTime, includesTime}: GoalFilter): Promise<GoalResult[]> { |
|||
let queries = []; |
|||
if (minTime != null) { |
|||
queries.push(`minTime=${minTime.toISOString()}`); |
|||
} |
|||
if (maxTime != null) { |
|||
queries.push(`maxTime=${maxTime.toISOString()}`); |
|||
} |
|||
if (includesTime != null) { |
|||
queries.push(`includesTime=${includesTime.toISOString()}`); |
|||
} |
|||
|
|||
const query = queries.length > 0 ? `?${queries.join("&")}` : ""; |
|||
|
|||
const data = await this.fetch("GET", `/api/goal/${query}`); |
|||
return data.goals; |
|||
} |
|||
|
|||
async createGoal(input: GoalInput): Promise<GoalResult> { |
|||
const data = await this.fetch("POST", "/api/goal/", input); |
|||
return data.goal; |
|||
} |
|||
|
|||
async updateGoal(id: string, update: GoalUpdate): Promise<GoalResult> { |
|||
const data = await this.fetch("PUT", `/api/goal/${id}`, update); |
|||
return data.goal; |
|||
} |
|||
|
|||
async deleteGoal(id: string): Promise<GoalResult> { |
|||
const data = await this.fetch("DELETE", `/api/goal/${id}`); |
|||
return data.goal; |
|||
} |
|||
|
|||
|
|||
|
|||
async findProject(id: string): Promise<ProjectResult> { |
|||
const data = await this.fetch("GET", `/api/project/${id}`); |
|||
return data.project; |
|||
} |
|||
|
|||
async listProjects({active, expiring}: ProjectFilter): Promise<ProjectResult[]> { |
|||
let queries = []; |
|||
if (active != null) { |
|||
queries.push(`active=${active}`); |
|||
} |
|||
if (expiring != null) { |
|||
queries.push(`expiring=${expiring}`); |
|||
} |
|||
|
|||
const query = queries.length > 0 ? `?${queries.join("&")}` : ""; |
|||
|
|||
const data = await this.fetch("GET", `/api/project/${query}`); |
|||
return data.projects; |
|||
} |
|||
|
|||
async createProject(input: ProjectInput): Promise<ProjectResult> { |
|||
const data = await this.fetch("POST", "/api/project/", input); |
|||
return data.project; |
|||
} |
|||
|
|||
async updateProject(id: string, update: ProjectUpdate): Promise<ProjectResult> { |
|||
const data = await this.fetch("PUT", `/api/project/${id}`, update); |
|||
return data.project; |
|||
} |
|||
|
|||
async deleteProject(id: string): Promise<ProjectResult> { |
|||
const data = await this.fetch("DELETE", `/api/project/${id}`); |
|||
return data.project; |
|||
} |
|||
|
|||
|
|||
|
|||
async findLog(id: string): Promise<LogResult> { |
|||
const data = await this.fetch("GET", `/api/log/${id}`); |
|||
return data.log; |
|||
} |
|||
|
|||
async listLogs({minTime, maxTime}: LogFilter): Promise<LogResult[]> { |
|||
let queries = []; |
|||
if (minTime != null) { |
|||
queries.push(`minTime=${minTime.toISOString()}`); |
|||
} |
|||
if (maxTime != null) { |
|||
queries.push(`maxTime=${maxTime.toISOString()}`); |
|||
} |
|||
|
|||
const query = queries.length > 0 ? `?${queries.join("&")}` : ""; |
|||
|
|||
const data = await this.fetch("GET", `/api/log/${query}`); |
|||
return data.logs; |
|||
} |
|||
|
|||
async createLog(input: LogInput): Promise<LogResult> { |
|||
const data = await this.fetch("POST", "/api/log/", input); |
|||
return data.log; |
|||
} |
|||
|
|||
async updateLog(id: string, update: LogUpdate): Promise<LogResult> { |
|||
const data = await this.fetch("PUT", `/api/log/${id}`, update); |
|||
return data.log; |
|||
} |
|||
|
|||
async deleteLog(id: string): Promise<LogResult> { |
|||
const data = await this.fetch("DELETE", `/api/log/${id}`); |
|||
return data.log; |
|||
} |
|||
|
|||
|
|||
|
|||
async findTask(id: string): Promise<TaskResult> { |
|||
const data = await this.fetch("GET", `/api/task/${id}`); |
|||
return data.task; |
|||
} |
|||
|
|||
async listTasks(active?: boolean): Promise<TaskResult[]> { |
|||
let query = (active != null) ? `?active=${active}` : ""; |
|||
|
|||
const data = await this.fetch("GET", `/api/task/${query}`); |
|||
return data.tasks; |
|||
} |
|||
|
|||
async createTask(input: TaskInput): Promise<TaskResult> { |
|||
const data = await this.fetch("POST", "/api/task/", input); |
|||
return data.task; |
|||
} |
|||
|
|||
async updateTask(id: string, update: TaskUpdate): Promise<TaskResult> { |
|||
const data = await this.fetch("PUT", `/api/task/${id}`, update); |
|||
return data.task; |
|||
} |
|||
|
|||
async deleteTask(id: string): Promise<TaskResult> { |
|||
const data = await this.fetch("DELETE", `/api/task/${id}`); |
|||
return data.task; |
|||
} |
|||
|
|||
|
|||
|
|||
async findGroup(id: string): Promise<GroupResult> { |
|||
const data = await this.fetch("GET", `/api/group/${id}`); |
|||
return data.group; |
|||
} |
|||
|
|||
async listGroups(): Promise<GroupResult[]> { |
|||
const data = await this.fetch("GET", "/api/group/"); |
|||
return data.groups; |
|||
} |
|||
|
|||
async createGroup(input: GroupInput): Promise<GroupResult> { |
|||
const data = await this.fetch("POST", "/api/group/", input); |
|||
return data.group; |
|||
} |
|||
|
|||
async updateGroup(id: string, update: GroupUpdate): Promise<GroupResult> { |
|||
const data = await this.fetch("PUT", `/api/group/${id}`, update); |
|||
return data.group; |
|||
} |
|||
|
|||
async deleteGroup(id: string): Promise<GroupResult> { |
|||
const data = await this.fetch("DELETE", `/api/group/${id}`); |
|||
return data.group; |
|||
} |
|||
|
|||
|
|||
|
|||
async findItem(id: string): Promise<ItemResult> { |
|||
const data = await this.fetch("GET", `/api/item/${id}`); |
|||
return data.item; |
|||
} |
|||
|
|||
async listItems(): Promise<ItemResult[]> { |
|||
const data = await this.fetch("GET", "/api/item/"); |
|||
return data.projects; |
|||
} |
|||
|
|||
async createItem(input: ItemInput): Promise<ItemResult> { |
|||
const data = await this.fetch("POST", "/api/item/", input); |
|||
return data.item; |
|||
} |
|||
|
|||
async updateItem(id: string, update: ItemUpdate): Promise<ItemResult> { |
|||
const data = await this.fetch("PUT", `/api/item/${id}`, update); |
|||
return data.item; |
|||
} |
|||
|
|||
async deleteItem(id: string): Promise<ItemResult> { |
|||
const data = await this.fetch("DELETE", `/api/item/${id}`); |
|||
return data.item; |
|||
} |
|||
|
|||
|
|||
|
|||
async fetch(method: string, path: string, body?: object) { |
|||
const fullPath = this.root + path; |
|||
const req: RequestInit = {method, headers: {}} |
|||
|
|||
if (body != null) { |
|||
const data = new Blob([JSON.stringify(body)]) |
|||
|
|||
req.headers["Content-Type"] = req; |
|||
req.headers["Content-Length"] = data.size; |
|||
req.body = data; |
|||
} |
|||
|
|||
console.warn("AUTH SKIPPED, remember to change back in prod!") |
|||
req.headers["Authorization"] = `Bearer ${await getJwt()}`; |
|||
|
|||
const res = await fetch(fullPath, req); |
|||
if (!res.ok) { |
|||
if ((res.headers.get("Content-Type") || "").includes("application/json")) { |
|||
const data = await res.json(); |
|||
throw new StuffLogError(data.errorCode, data.errorMessage) |
|||
} else { |
|||
const text = await res.text(); |
|||
throw new StuffLogError(res.status, text) |
|||
} |
|||
} |
|||
|
|||
return res.json(); |
|||
} |
|||
} |
|||
|
|||
export class StuffLogError { |
|||
public code: number |
|||
public message: string |
|||
|
|||
constructor(code: number, message: string) { |
|||
this.code = code; |
|||
this.message = message; |
|||
} |
|||
|
|||
toString() { |
|||
return `Error ${this.code}: ${this.message}`; |
|||
} |
|||
} |
|||
|
|||
const stuffLogClient = new StufflogClient(""); |
|||
export default stuffLogClient |
@ -0,0 +1,53 @@ |
|||
<script lang="ts"> |
|||
import { createEventDispatcher } from "svelte"; |
|||
import type { ModalData } from "../stores/modal"; |
|||
import modalStore from "../stores/modal"; |
|||
|
|||
export let open: ModalData = {name: "none"}; |
|||
export let disabled: boolean = false; |
|||
export let compact: boolean = false; |
|||
|
|||
const dispatch = createEventDispatcher(); |
|||
|
|||
function handleClick() { |
|||
dispatch("click", {open}); |
|||
|
|||
if (open.name !== "none") { |
|||
modalStore.set(open); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="boi" class:disabled class:compact on:click={handleClick}><slot></slot></div> |
|||
|
|||
<style> |
|||
div.boi { |
|||
border: 6px dashed; |
|||
padding: 0.5em; |
|||
margin: 1em 0.5ch; |
|||
text-align: center; |
|||
color: #777; |
|||
border-color: #333; |
|||
cursor: pointer; |
|||
border-bottom-right-radius: 0.25em; |
|||
font-size: 2em; |
|||
-webkit-user-select: none; |
|||
-moz-user-select: none; |
|||
} |
|||
div.boi:hover { |
|||
color: #AAA; |
|||
border-color: #444; |
|||
} |
|||
|
|||
div.boi.disabled { |
|||
color: #333; |
|||
border-color: #222; |
|||
cursor: wait; |
|||
} |
|||
|
|||
div.boi.compact { |
|||
margin: 0; |
|||
border-width: 4px; |
|||
padding: 0.25em; |
|||
} |
|||
</style> |
@ -0,0 +1,17 @@ |
|||
<script lang="ts"> |
|||
export let time: Date | string = new Date(); |
|||
|
|||
let timeStr = ""; |
|||
|
|||
function formatTime(time: Date): string { |
|||
const pad = (n:number) => n < 10 ? '0'+n : n.toString(); |
|||
|
|||
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())} ${pad(time.getHours())}:${pad(time.getMinutes())}` |
|||
} |
|||
|
|||
$: timeStr = formatTime(new Date(time)); |
|||
</script> |
|||
|
|||
<span>{timeStr}</span> |
|||
|
|||
<style></style> |
@ -0,0 +1,87 @@ |
|||
<script lang="ts"> |
|||
export let startTime: Date | string = new Date(); |
|||
export let endTime: Date | string = new Date(); |
|||
|
|||
let started = false; |
|||
let overdue = false; |
|||
let danger = false; |
|||
let amount = 0; |
|||
let amountStr = "0"; |
|||
let unit = "days"; |
|||
let titleTimeStr = ""; |
|||
|
|||
function formatTime(time: Date): string { |
|||
const pad = (n:number) => n < 9 ? '0'+n : n.toString(); |
|||
|
|||
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())}` |
|||
} |
|||
|
|||
$: { |
|||
const now = new Date(); |
|||
|
|||
overdue = false; |
|||
unit = "days"; |
|||
|
|||
const st = (startTime instanceof Date) ? startTime : new Date(startTime); |
|||
const et = (endTime instanceof Date) ? endTime : new Date(endTime); |
|||
|
|||
if (now < st) { |
|||
started = false; |
|||
amount = (st.getTime() - now.getTime()) / 86400000 |
|||
} else { |
|||
started = true; |
|||
amount = (et.getTime() - now.getTime()) / 86400000 |
|||
} |
|||
|
|||
if (amount < 0) { |
|||
overdue = true; |
|||
amount = -amount; |
|||
} |
|||
|
|||
danger = (!overdue && started && amount <= 3); |
|||
|
|||
if (amount < 2) { |
|||
amount *= 24; |
|||
unit = "hours" |
|||
} |
|||
if (amount < 2) { |
|||
amount *= 60; |
|||
unit = "minutes"; |
|||
} |
|||
|
|||
amount = Math.floor(amount); |
|||
if (amount <= 1) { |
|||
unit = unit.slice(0, -1); |
|||
} |
|||
|
|||
if (amount < 1) { |
|||
amountStr = "< 1" |
|||
} else { |
|||
amountStr = amount.toString() |
|||
} |
|||
|
|||
titleTimeStr = `${formatTime(new Date(startTime))} – ${formatTime(new Date(endTime))}` |
|||
} |
|||
</script> |
|||
|
|||
<span title={titleTimeStr}> |
|||
{#if (overdue)} |
|||
<span class="overdue">{amountStr} {unit} ago</span> |
|||
{:else if (started)} |
|||
<span class:danger class="started">{amountStr} {unit} left</span> |
|||
{:else} |
|||
<span class="pending">In {amountStr} {unit}</span> |
|||
{/if} |
|||
</span> |
|||
|
|||
<style> |
|||
span.pending { |
|||
color: #2797e2; |
|||
} |
|||
span.danger { |
|||
color: #e28127; |
|||
} |
|||
span.overdue { |
|||
color: #666666; |
|||
} |
|||
</style> |
@ -0,0 +1,93 @@ |
|||
<script lang="ts"> |
|||
import type { IconName } from "../external/icons"; |
|||
import type { GoalResult } from "../models/goal"; |
|||
import type { ModalData } from "../stores/modal"; |
|||
import DaysLeft from "./DaysLeft.svelte"; |
|||
import Icon from "./Icon.svelte"; |
|||
import Option from "./Option.svelte"; |
|||
import OptionRow from "./OptionRow.svelte"; |
|||
import Progress from "./Progress.svelte"; |
|||
|
|||
export let goal: GoalResult = null; |
|||
export let showAllOptions = false; |
|||
|
|||
let iconName: IconName = "question"; |
|||
let mdGoalEdit: ModalData; |
|||
let mdGoalDelete: ModalData; |
|||
|
|||
$: iconName = goal.group.icon as IconName; |
|||
$: mdGoalEdit = {name:"goal.edit", goal}; |
|||
$: mdGoalDelete = {name:"goal.delete", goal}; |
|||
</script> |
|||
|
|||
<div class="goal" class:full={showAllOptions}> |
|||
<div class="icon"><Icon block name={iconName} /></div> |
|||
<div class="body"> |
|||
<div class="header"> |
|||
<div class="name">{goal.name}</div> |
|||
<div class="times"> |
|||
<DaysLeft startTime={goal.startTime} endTime={goal.endTime} /> |
|||
</div> |
|||
</div> |
|||
{#if showAllOptions} |
|||
<div class="description"> |
|||
<p>{goal.description}</p> |
|||
</div> |
|||
<OptionRow> |
|||
<Option open={mdGoalEdit}>Edit</Option> |
|||
<Option open={mdGoalDelete}>Delete</Option> |
|||
</OptionRow> |
|||
{/if} |
|||
<div class="progress"> |
|||
<Progress count={goal.completedAmount} target={goal.amount} /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
div.goal { |
|||
display: flex; |
|||
flex-direction: row; |
|||
padding-bottom: 0.5em; |
|||
} |
|||
div.goal.full { |
|||
padding-bottom: 1em; |
|||
} |
|||
div.icon { |
|||
font-size: 2em; |
|||
padding: 0 0.5ch; |
|||
width: 2ch; |
|||
padding-top: 0.125em; |
|||
color: #333; |
|||
} |
|||
div.body { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
} |
|||
div.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
} |
|||
|
|||
div.name { |
|||
font-size: 1em; |
|||
margin: auto 0; |
|||
vertical-align: middle; |
|||
font-weight: 100; |
|||
} |
|||
div.times { |
|||
margin-left: auto; |
|||
margin-right: 0.25ch; |
|||
} |
|||
|
|||
div.progress { |
|||
padding-top: 0.125em; |
|||
font-size: 1.25em; |
|||
} |
|||
|
|||
div.description > p { |
|||
padding: 0; |
|||
margin: 0.25em 0; |
|||
} |
|||
</style> |
@ -0,0 +1,84 @@ |
|||
<script lang="ts"> |
|||
import type { IconName } from "../external/icons"; |
|||
import type { GroupResult } from "../models/group"; |
|||
import type { ModalData } from "../stores/modal"; |
|||
import DaysLeft from "./DaysLeft.svelte"; |
|||
import Icon from "./Icon.svelte"; |
|||
import ItemEntry from "./ItemEntry.svelte"; |
|||
import Option from "./Option.svelte"; |
|||
import OptionRow from "./OptionRow.svelte"; |
|||
import TaskEntry from "./TaskEntry.svelte"; |
|||
|
|||
export let group: GroupResult = null; |
|||
export let showAllOptions: boolean = false; |
|||
|
|||
let iconName: IconName = "question"; |
|||
let mdItemAdd: ModalData; |
|||
let mdGroupEdit: ModalData; |
|||
let mdGroupDelete: ModalData; |
|||
|
|||
$: iconName = group.icon as IconName; |
|||
$: mdItemAdd = {name:"item.add", group}; |
|||
$: mdGroupEdit = {name:"group.edit", group}; |
|||
$: mdGroupDelete = {name:"group.delete", group}; |
|||
</script> |
|||
|
|||
<div class="group"> |
|||
<div class="icon"><Icon block name={iconName} /></div> |
|||
<div class="body"> |
|||
<div class="header"> |
|||
<div class="name">{group.name}</div> |
|||
</div> |
|||
{#if showAllOptions} |
|||
<div class="description"> |
|||
<p>{group.description}</p> |
|||
</div> |
|||
<OptionRow> |
|||
<Option open={mdItemAdd}>Add Item</Option> |
|||
<Option open={mdGroupEdit}>Edit</Option> |
|||
<Option open={mdGroupDelete}>Delete</Option> |
|||
</OptionRow> |
|||
{/if} |
|||
<div class="list" class:full={showAllOptions}> |
|||
{#each group.items as item (item.id)} |
|||
<ItemEntry item={item} group={group} /> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
div.group { |
|||
display: flex; |
|||
flex-direction: row; |
|||
padding-bottom: 1em; |
|||
} |
|||
div.icon { |
|||
font-size: 2em; |
|||
padding: 0 0.5ch; |
|||
width: 2ch; |
|||
padding-top: 0.125em; |
|||
color: #333; |
|||
} |
|||
div.body { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
} |
|||
div.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
} |
|||
|
|||
div.name { |
|||
font-size: 1em; |
|||
font-weight: 100; |
|||
margin: auto 0; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
div.description > p { |
|||
padding: 0; |
|||
margin: 0.25em 0; |
|||
} |
|||
</style> |
@ -0,0 +1,30 @@ |
|||
<script lang="ts"> |
|||
import groupStore from "../stores/group"; |
|||
|
|||
export let value = ""; |
|||
export let name = ""; |
|||
export let disabled = false; |
|||
|
|||
$: { |
|||
if ($groupStore.stale && !$groupStore.loading) { |
|||
groupStore.load(); |
|||
} |
|||
} |
|||
|
|||
$: { |
|||
if (!disabled && $groupStore.groups.length > 0 && value === "") { |
|||
const nonEmpty = $groupStore.groups.find(g => g.items.length > 0); |
|||
if (nonEmpty != null) { |
|||
value = nonEmpty.id; |
|||
} else { |
|||
value = $groupStore.groups[0].id; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<select disabled={disabled || $groupStore.loading} name={name} bind:value={value}> |
|||
{#each $groupStore.groups as group (group.id)} |
|||
<option value={group.id} selected={group.id === value}>{group.name} ({group.items.length} items)</option> |
|||
{/each} |
|||
</select> |
@ -0,0 +1,22 @@ |
|||
<script lang="ts"> |
|||
import Icon from "fa-svelte" |
|||
import icons from "../external/icons"; |
|||
import type { IconName } from "../external/icons"; |
|||
|
|||
export let name: IconName = "question"; |
|||
export let block: boolean = false; |
|||
</script> |
|||
|
|||
{#if block} |
|||
<div> |
|||
<Icon class="activity-icon" icon={icons[name] || icons.question} /> |
|||
</div> |
|||
{:else} |
|||
<Icon class="activity-icon" icon={icons[name] || icons.question} /> |
|||
{/if} |
|||
|
|||
<style> |
|||
div { |
|||
margin: auto; |
|||
} |
|||
</style> |
@ -0,0 +1,56 @@ |
|||
<script lang="ts"> |
|||
import type { IconName } from "../external/icons"; |
|||
import { iconNames } from "../external/icons"; |
|||
import Icon from "./Icon.svelte"; |
|||
|
|||
export let value: IconName; |
|||
export let disabled: boolean; |
|||
</script> |
|||
|
|||
<div class:disabled class="icon-select"> |
|||
{#each iconNames as iconName (iconName)} |
|||
<div class="icon-item" class:selected={value===iconName} on:click={() => {if (!disabled) { value = iconName }}}> |
|||
<Icon name={iconName} /> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
div.icon-select { |
|||
background: #222; |
|||
margin: 0; |
|||
padding: 0; |
|||
border-radius: 0.05em; |
|||
margin-bottom: 0.5em; |
|||
} |
|||
|
|||
div.icon-select.disabled { |
|||
background: #444; |
|||
} |
|||
|
|||
div.icon-item { |
|||
display: inline-block; |
|||
box-sizing: border-box; |
|||
width: calc(100% / 8); |
|||
padding: 0.35em 0 0.25em 0; |
|||
text-align: center; |
|||
|
|||
cursor: pointer; |
|||
} |
|||
div.icon-item:hover { |
|||
background-color: #292929; |
|||
} |
|||
div.icon-item.selected { |
|||
background-color: rgb(18, 63, 75); |
|||
} |
|||
div.icon-item.selected:hover { |
|||
background-color: rgb(24, 83, 99); |
|||
} |
|||
div.icon-select.disabled > div.icon-item { |
|||
background-color: #444; |
|||
cursor: default; |
|||
} |
|||
div.icon-select.disabled > div.icon-item.selected { |
|||
background-color: #555; |
|||
} |
|||
</style> |
@ -0,0 +1,86 @@ |
|||
<script lang="ts"> |
|||
import type { IconName } from "../external/icons"; |
|||
import type { GroupResult } from "../models/group"; |
|||
import type { default as Item } from "../models/item"; |
|||
import type { ModalData } from "../stores/modal"; |
|||
import Option from "./Option.svelte"; |
|||
import OptionRow from "./OptionRow.svelte"; |
|||
|
|||
export let item: Item = null; |
|||
export let group: GroupResult = null; |
|||
|
|||
let mdItemEdit: ModalData; |
|||
let mdItemDelete: ModalData; |
|||
|
|||
$: mdItemEdit = {name: "item.edit", item, group}; |
|||
$: mdItemDelete = {name: "item.delete", item, group}; |
|||
</script> |
|||
|
|||
<div class="item"> |
|||
<div class="body"> |
|||
<div class="header"> |
|||
<div class="icon"> |
|||
{item.groupWeight} |
|||
</div> |
|||
<div class="name">{item.name}</div> |
|||
</div> |
|||
<div class="description"> |
|||
<p>{item.description}</p> |
|||
|
|||
<OptionRow> |
|||
<Option open={mdItemEdit}>Edit</Option> |
|||
<Option open={mdItemDelete}>Delete</Option> |
|||
</OptionRow> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
div.item { |
|||
display: flex; |
|||
flex-direction: row; |
|||
margin: 0.25em 0 0.75em 0; |
|||
} |
|||
div.body { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
} |
|||
div.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
background: #333; |
|||
} |
|||
|
|||
div.icon { |
|||
display: flex; |
|||
flex-direction: column; |
|||
font-size: 1em; |
|||
padding: 0.125em .5ch; |
|||
min-width: 2ch; |
|||
text-align: center; |
|||
|
|||
margin-right: 0.5em; |
|||
background: #444; |
|||
color: #CCC; |
|||
} |
|||
|
|||
div.name { |
|||
font-size: 1em; |
|||
font-weight: 100; |
|||
margin: auto 0; |
|||
vertical-align: middle; |
|||
padding: 0.125em .5ch; |
|||
} |
|||
|
|||
div.description { |
|||
padding: 0.25em 1ch; |
|||
background: #222; |
|||
color: #aaa; |
|||
border-bottom-right-radius: 0.5em; |
|||
} |
|||
div.description p { |
|||
padding: 0; |
|||
margin: 0.25em 0; |
|||
} |
|||
</style> |
@ -0,0 +1,31 @@ |
|||
<script lang="ts"> |
|||
import groupStore from "../stores/group"; |
|||
|
|||
export let value = ""; |
|||
export let name = ""; |
|||
|
|||
$: { |
|||
if ($groupStore.stale && !$groupStore.loading) { |
|||
groupStore.load(); |
|||
} |
|||
} |
|||
|
|||
$: { |
|||
if ($groupStore.groups.length > 0 && value === "") { |
|||
const nonEmpty = $groupStore.groups.find(g => g.items.length > 0); |
|||
if (nonEmpty != null) { |
|||
value = nonEmpty.items[0].id; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<select name={name} bind:value={value} disabled={$groupStore.loading}> |
|||
{#each $groupStore.groups as group (group.id)} |
|||
<optgroup label={group.name}> |
|||
{#each group.items as item (item.id)} |
|||
<option value={item.id} selected={item.id === value}>{item.name} ({item.groupWeight})</option> |
|||
{/each} |
|||
</optgroup> |
|||
{/each} |
|||
</select> |
@ -0,0 +1,95 @@ |
|||
<script lang="ts"> |
|||
import type { IconName } from "../external/icons"; |
|||
import type { LogResult } from "../models/log"; |
|||
import type { ModalData } from "../stores/modal"; |
|||
import { formatTime } from "../utils/time"; |
|||
import Icon from "./Icon.svelte"; |
|||
import Option from "./Option.svelte"; |
|||
import OptionRow from "./OptionRow.svelte"; |
|||
|
|||
export let log: LogResult = null; |
|||
|
|||
let taskIconName: IconName = "question"; |
|||
let mdLogEdit: ModalData; |
|||
let mdLogDelete: ModalData; |
|||
|
|||
$: taskIconName = log.task.icon as IconName; |
|||
$: mdLogEdit = {name: "log.edit", log}; |
|||
$: mdLogDelete = {name: "log.delete", log}; |
|||
</script> |
|||
|
|||
<div class="log"> |
|||
<div class="body"> |
|||
<div class="header"> |
|||
<div class="icon"> |
|||
<Icon name={taskIconName} /> |
|||
</div> |
|||
<div class="name">{log.task.name}</div> |
|||
<div class="times">{formatTime(log.loggedTime)}</div> |
|||
</div> |
|||
<div class="description"> |
|||
<p>{log.description}</p> |
|||
|
|||
<OptionRow> |
|||
<Option open={mdLogEdit}>Edit Log</Option> |
|||
<Option open={mdLogDelete}>Delete Log</Option> |
|||
</OptionRow> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
div.log { |
|||
display: flex; |
|||
flex-direction: row; |
|||
margin: 0.25em 0; |
|||
} |
|||
div.icon { |
|||
display: flex; |
|||
flex-direction: column; |
|||
font-size: 1em; |
|||
padding: 0.125em .5ch; |
|||
padding-top: 0.2em; |
|||
|
|||
margin-right: 0.5em; |
|||
background: #444; |
|||
color: #CCC; |
|||
} |
|||
div.body { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
} |
|||
div.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
background: #333; |
|||
} |
|||
|
|||
div.name { |
|||
font-size: 1em; |
|||
font-weight: 100; |
|||
margin: auto 0; |
|||
vertical-align: middle; |
|||
padding: 0.125em .5ch; |
|||
} |
|||
div.times { |
|||
margin-left: auto; |
|||
margin-right: 0.25ch; |
|||
} |
|||
|
|||
div.description { |
|||
padding: 0.25em 1ch; |
|||
background: #222; |
|||
color: #aaa; |
|||
border-bottom-right-radius: 0.5em; |
|||
} |
|||
div.description p { |
|||
padding: 0; |
|||
margin: 0.25em 0; |
|||
} |
|||
|
|||
div.log { |
|||
padding: 0.25em 1ch; |
|||
} |
|||
</style> |
@ -0,0 +1,44 @@ |
|||
<script lang="ts"> |
|||
import { link } from "svelte-routing"; |
|||
|
|||
export let location: string = window.location.pathname.split("?")[0]; |
|||
|
|||
function updateLocation() { |
|||
setTimeout(() => { |
|||
location = window.location.pathname.split("?")[0]; |
|||
}, 0); |
|||
} |
|||
|
|||
$: selected = { |
|||
home: location == "/", |
|||
goals: location.startsWith("/goals"), |
|||
projects: location.startsWith("/projects"), |
|||
items: location.startsWith("/items"), |
|||
logs: location.startsWith("/logs"), |
|||
} |
|||
</script> |
|||
|
|||
<nav> |
|||
<a on:click={updateLocation} class:selected={selected.home} use:link href="/">Stufflog</a> |
|||
<a on:click={updateLocation} class:selected={selected.goals} use:link href="/goals">Goals</a> |
|||
<a on:click={updateLocation} class:selected={selected.projects} use:link href="/projects">Projects</a> |
|||
<a on:click={updateLocation} class:selected={selected.items} use:link href="/items">Items</a> |
|||
<a on:click={updateLocation} class:selected={selected.logs} use:link href="/logs">Logs</a> |
|||
</nav> |
|||
|
|||
<style> |
|||
nav { |
|||
margin: 0; |
|||
text-align: center; |
|||
} |
|||
|
|||
a { |
|||
display: inline-block; |
|||
padding: 0.25em; |
|||
color: #555; |
|||
font-size: 1em; |
|||
} |
|||
a.selected { |
|||
color: #AAA; |
|||
} |
|||
</style> |
@ -0,0 +1,215 @@ |
|||
<script lang="ts"> |
|||
import { createEventDispatcher, onMount } from 'svelte'; |
|||
import Icon from './Icon.svelte'; |
|||
|
|||
export let title: string = ""; |
|||
export let wide: boolean = false; |
|||
export let error: string | null = null; |
|||
export let closable: boolean = false; |
|||
export let show: boolean = false; |
|||
|
|||
onMount(() => { |
|||
const listener = (ev: KeyboardEvent) => { |
|||
console.log(ev.key); |
|||
|
|||
if ((ev.ctrlKey || ev.altKey) && (ev.key === "Escape" || ev.key.toLowerCase() === "q")) { |
|||
dispatch("close"); |
|||
} |
|||
} |
|||
|
|||
document.addEventListener("keyup", listener); |
|||
|
|||
return () => { |
|||
document.removeEventListener("keyup", listener); |
|||
} |
|||
}) |
|||
|
|||
const dispatch = createEventDispatcher(); |
|||
</script> |
|||
|
|||
{#if show} |
|||
<div class="modal-background"> |
|||
<div class="modal" class:wide> |
|||
<div class="header"> |
|||
<div class="title" class:noclose={!closable}>{title}</div> |
|||
{#if (closable)} |
|||
<div class="x"> |
|||
<div class="button" on:click={() => dispatch("close")}> |
|||
<Icon name="times" /> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
<hr /> |
|||
{#if (error != null)} |
|||
<div class="error">{error}</div> |
|||
{/if} |
|||
<div class="body"> |
|||
<slot></slot> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
div.modal-background { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
background: rgba(0,0,0,0.3); |
|||
} |
|||
|
|||
div.modal { |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
width: calc(100vw - 4em); |
|||
max-width: 40ch; |
|||
max-height: calc(100vh - 4em); |
|||
overflow: auto; |
|||
transform: translate(-50%,-50%); |
|||
padding: 1em; |
|||
border-radius: 0.2em; |
|||
|
|||
background: #333; |
|||
} |
|||
div.modal.wide { |
|||
max-width: 60ch; |
|||
} |
|||
|
|||
div.modal :global(hr) { |
|||
border: 0.5px solid gray; |
|||
margin: 0; |
|||
} |
|||
|
|||
div.error { |
|||
margin: 0.5em; |
|||
padding: 0.5em; |
|||
|
|||
border: 1px solid rgb(204, 65, 65); |
|||
border-radius: 0.2em; |
|||
background-color: rgb(133, 39, 39); |
|||
color: rgb(211, 141, 141); |
|||
|
|||
animation: fadein 0.5s; |
|||
} |
|||
|
|||
div.body { |
|||
margin: 1em 0.25ch; |
|||
} |
|||
|
|||
div.title { |
|||
color: #CCC; |
|||
line-height: 1em; |
|||
} |
|||
|
|||
div.title.noclose { |
|||
margin-bottom: 1.2em; |
|||
} |
|||
|
|||
div.x { |
|||
position: relative; |
|||
line-height: 1em; |
|||
top: -1em; |
|||
text-align: right; |
|||
} |
|||
div.x div.button { |
|||
color: #CCC; |
|||
display: inline-block; |
|||
padding: 0em 0.5ch 0.1em 0.5ch; |
|||
line-height: 1em; |
|||
user-select: none; |
|||
cursor: pointer; |
|||
} |
|||
div.x div.button:hover { |
|||
color: #FFF; |
|||
} |
|||
|
|||
div.modal :global(button) { |
|||
display: inline-block; |
|||
padding: 0.25em 0.75ch 0.26em 0.75ch; |
|||
margin: 0.75em 0.25ch 0.25em 0.25ch; |
|||
|
|||
background: none; |
|||
border: none; |
|||
border-radius: 0.2em; |
|||
color: #CCC; |
|||
|
|||
cursor: pointer; |
|||
} |
|||
|
|||
div.modal :global(button:hover), div.modal :global(button:focus) { |
|||
background: #222; |
|||
color: #FFF; |
|||
} |
|||
|
|||
div.modal :global(label) { |
|||
padding: 0 0 0.125em 0.25ch; |
|||
font-size: 0.75em; |
|||
user-select: none; |
|||
-webkit-user-select: none; |
|||
-moz-user-select: none; |
|||
} |
|||
|
|||
div.modal :global(input), div.modal :global(select), div.modal :global(textarea) { |
|||
width: 100%; |
|||
margin-bottom: 0.5em; |
|||
|
|||
background: #222; |
|||
color: #777; |
|||
border: none; |
|||
outline: none; |
|||
resize: none; |
|||
} |
|||
div.modal :global(select) { |
|||
padding-left: 0.5ch; |
|||
} |
|||
div.modal :global(input:disabled) { |
|||
background: #444; |
|||
color: #aaa; |
|||
} |
|||
div.modal :global(textarea) { |
|||
height: 6em; |
|||
} |
|||
div.modal :global(textarea:disabled) { |
|||
background: #444; |
|||
color: #aaa; |
|||
} |
|||
|
|||
div.modal :global(input:last-of-type) { |
|||
margin-bottom: 1em; |
|||
} |
|||
div.modal :global(input.nolast) { |
|||
margin-bottom: 0.5em; |
|||
} |
|||
|
|||
div.modal :global(input[type="checkbox"]) { |
|||
width: initial; |
|||
display: inline-block; |
|||
} |
|||
div.modal :global(input[type="checkbox"] + label) { |
|||
width: initial; |
|||
display: inline-block; |
|||
padding: 0; |
|||
margin: 0; |
|||
} |
|||
|
|||
div.modal :global(input:focus), div.modal :global(select:focus), div.modal :global(textarea:focus) { |
|||
background: #111; |
|||
color: #CCC; |
|||
border: none; |
|||
outline: none; |
|||
} |
|||
|
|||
div.modal :global(p) { |
|||
margin: 0.25em 1ch 1em 1ch; |
|||
font-size: 0.9em; |
|||
} |
|||
|
|||
@keyframes fadein { |
|||
from { opacity: 0; } |
|||
to { opacity: 1; } |
|||
} |
|||
</style> |
@ -0,0 +1,10 @@ |
|||
<script lang="ts"> |
|||
import type { ModalData } from "../stores/modal"; |
|||
import modalStore from "../stores/modal"; |
|||
|
|||
export let name: ModalData["name"] = "none"; |
|||
</script> |
|||
|
|||
{#if $modalStore.name === name} |
|||
<slot></slot> |
|||
{/if} |
@ -0,0 +1,38 @@ |
|||
<script lang="ts"> |
|||
import { createEventDispatcher } from "svelte"; |
|||
import type { ModalData } from "../stores/modal"; |
|||
import modalStore from "../stores/modal"; |
|||
|
|||
export let open: ModalData = {name: "none"}; |
|||
|
|||
const dispatch = createEventDispatcher(); |
|||
|
|||
function handleClick() { |
|||
dispatch("click", {open}); |
|||
|
|||
if (open.name !== "none") { |
|||
modalStore.set(open); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div on:click={handleClick} class="option"><slot></slot></div> |
|||
|
|||
<style> |
|||
div.option { |
|||
display: inline-block; |
|||
font-size: 0.9em; |
|||
padding: 0.125em 0.75ch; |
|||
cursor: pointer; |
|||
color: #aa8822; |
|||
|
|||
user-select: none; |
|||
-webkit-user-select: none; |
|||
-moz-user-select: none; |
|||
} |
|||
|
|||
div.option:hover { |
|||
color: #FC1; |
|||
text-decoration: underline; |
|||
} |
|||
</style> |
@ -0,0 +1,10 @@ |
|||
<div class="option-row"> |
|||
<slot></slot> |
|||
</div> |
|||
|
|||
<style> |
|||
div.option-row { |
|||
margin-left: -0.5ch; |
|||
margin-right: -0.5ch; |
|||
} |
|||
</style> |
@ -0,0 +1,80 @@ |
|||
<script lang="ts" context="module"> |
|||
const COLORS = [ |
|||
"none", |
|||
"bronze", |
|||
"silver", |
|||
"gold", |
|||
"diamond" |
|||
] |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
export let target = 1; |
|||
export let count = 0; |
|||
|
|||
let offClass = COLORS[0]; |
|||
let onClass = COLORS[1]; |
|||
let ons = 0; |
|||
let offs = 1; |
|||
|
|||
$: { |
|||
let level = Math.floor(count / target); |
|||
if (level >= COLORS.length - 1) { |
|||
offs = 0; |
|||
ons = target; |
|||
offClass = "gold"; |
|||
onClass = "diamond"; |
|||
} else { |
|||
if (count > 0 && count == (level * target)) { |
|||
level -= 1; |
|||
} |
|||
|
|||
ons = count - (level * target); |
|||
offs = target - ons; |
|||
offClass = COLORS[level]; |
|||
onClass = COLORS[level + 1]; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="bar"> |
|||
{#each {length: ons} as _} |
|||
<div class={"on " + onClass}></div> |
|||
{/each} |
|||
{#each {length: offs} as _} |
|||
<div class={"off " + offClass}></div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
div.bar { |
|||
display: flex; |
|||
flex-direction: row; |
|||
margin: 0; |
|||
box-sizing: border-box; |
|||
width: 100%; |
|||
height: 1em; |
|||
} |
|||
|
|||
div.bar > div { |
|||
flex-grow: 1; |
|||
flex-basis: 0; |
|||
display: inline-block; |
|||
box-sizing: border-box; |
|||
border: 0.1px solid #000; |
|||
} |
|||
|
|||
div.none { background-color: #555555; } |
|||
div.bronze { background-color: #f4b083; } |
|||
div.silver { background-color: #d8dce4; } |
|||
div.gold { background-color: #ffd966; } |
|||
div.diamond { background-color: #84f5ff; } |
|||
|
|||
div.on { |
|||
opacity: 0.75; |
|||
} |
|||
|
|||
div.off { |
|||
opacity: 0.33; |
|||
} |
|||
</style> |
@ -0,0 +1,94 @@ |
|||
<script lang="ts"> |
|||
import type { IconName } from "../external/icons"; |
|||
import type { ProjectResult } from "../models/project"; |
|||
import type { ModalData } from "../stores/modal"; |
|||
import DaysLeft from "./DaysLeft.svelte"; |
|||
import Icon from "./Icon.svelte"; |
|||
import Option from "./Option.svelte"; |
|||
import OptionRow from "./OptionRow.svelte"; |
|||
import TaskEntry from "./TaskEntry.svelte"; |
|||
|
|||
export let project: ProjectResult = null; |
|||
export let showAllOptions: boolean = false; |
|||
|
|||
let iconName: IconName = "question"; |
|||
let mdAddTask: ModalData; |
|||
let mdProjectAdd: ModalData; |
|||
let mdProjectEdit: ModalData; |
|||
let mdProjectDelete: ModalData; |
|||
|
|||
$: iconName = project.icon as IconName; |
|||
$: mdAddTask = {name:"task.add", project}; |
|||
$: mdProjectAdd = {name:"project.add"}; |
|||
$: mdProjectEdit = {name:"project.edit", project}; |
|||
$: mdProjectDelete = {name:"project.delete", project}; |
|||
</script> |
|||
|
|||
<div class="project"> |
|||
<div class="icon"><Icon block name={iconName} /></div> |
|||
<div class="body"> |
|||
<div class="header"> |
|||
<div class="name">{project.name}</div> |
|||
{#if (project.endTime != null)} |
|||
<div class="times"> |
|||
<DaysLeft startTime={project.createdTime} endTime={project.endTime} /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
{#if showAllOptions} |
|||
<div class="description"> |
|||
<p>{project.description}</p> |
|||
</div> |
|||
<OptionRow> |
|||
<Option open={mdAddTask}>Add Task</Option> |
|||
<Option open={mdProjectEdit}>Edit</Option> |
|||
<Option open={mdProjectDelete}>Delete</Option> |
|||
</OptionRow> |
|||
{/if} |
|||
<div class="list" class:full={showAllOptions}> |
|||
{#each project.tasks as task (task.id)} |
|||
<TaskEntry showAllOptions={showAllOptions} task={task} /> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
div.project { |
|||
display: flex; |
|||
flex-direction: row; |
|||
padding-bottom: 1em; |
|||
} |
|||
div.icon { |
|||
font-size: 2em; |
|||
padding: 0 0.5ch; |
|||
width: 2ch; |
|||
padding-top: 0.125em; |
|||
color: #333; |
|||
} |
|||
div.body { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
} |
|||
div.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
} |
|||
|
|||
div.name { |
|||
font-size: 1em; |
|||
font-weight: 100; |
|||
margin: auto 0; |
|||
vertical-align: middle; |
|||
} |
|||
div.times { |
|||
margin-left: auto; |
|||
margin-right: 0.25ch; |
|||
} |
|||
|
|||
div.description > p { |
|||
padding: 0; |
|||
margin: 0.25em 0; |
|||
} |
|||
</style> |
@ -0,0 +1,161 @@ |
|||
<script lang="ts"> |
|||
import type { IconName } from "../external/icons"; |
|||
import type { TaskResult } from "../models/task"; |
|||
import type { ModalData } from "../stores/modal"; |
|||
import DateSpan from "./DateSpan.svelte"; |
|||
import DaysLeft from "./DaysLeft.svelte"; |
|||
import Icon from "./Icon.svelte"; |
|||
import Option from "./Option.svelte"; |
|||
import OptionRow from "./OptionRow.svelte"; |
|||
|
|||
export let task: TaskResult = null; |
|||
export let showAllOptions: boolean = false; |
|||
|
|||
let itomIconName: IconName = "question"; |
|||
let showLogs = false; |
|||
let mdLogAdd: ModalData; |
|||
let mdTaskEdit: ModalData; |
|||
let mdTaskDelete: ModalData; |
|||
|
|||
function toggleShowLogs() { |
|||
showLogs = !showLogs; |
|||
} |
|||
|
|||
$: itomIconName = task.item.icon as IconName; |
|||
$: mdLogAdd = {name: "log.add", task}; |
|||
$: mdTaskEdit = {name: "task.edit", task}; |
|||
$: mdTaskDelete = {name: "task.delete", task}; |
|||
</script> |
|||
|
|||
<div class="task"> |
|||
<div class="body"> |
|||
<div class="header"> |
|||
{#if !task.active} |
|||
<div class="icon done"> |
|||
<Icon name="check" /> |
|||
</div> |
|||
{:else} |
|||
<div class="icon"> |
|||
{task.completedAmount} / {task.itemAmount} |
|||
</div> |
|||
{/if} |
|||
<div class="name">{task.name}</div> |
|||
{#if (task.endTime != null)} |
|||
<div class="times"> |
|||
<DaysLeft startTime={task.createdTime} endTime={task.endTime} /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
<div class="description"> |
|||
<p>{task.description}</p> |
|||
<div class="item"> |
|||
<div class="item-icon"> |
|||
<Icon name={itomIconName} /> |
|||
</div> |
|||
<div class="item-name">{task.item.name} ({task.item.groupWeight})</div> |
|||
</div> |
|||
<OptionRow> |
|||
{#if task.logs.length > 0} |
|||
<Option on:click={toggleShowLogs}>{showLogs ? "Hide Logs" : "Show Logs"}</Option> |
|||
{/if} |
|||
<Option open={mdLogAdd}>Add Log</Option> |
|||
{#if showAllOptions} |
|||
<Option open={mdTaskEdit}>Edit</Option> |
|||
<Option open={mdTaskDelete}>Delete</Option> |
|||
{/if} |
|||
</OptionRow> |
|||
{#if showLogs && task.logs.length > 0} |
|||
<div class="log-list"> |
|||
{#each task.logs as log (log.id)} |
|||
<div class="log"> |
|||
<div class="log-time"><DateSpan time={log.loggedTime} /></div> |
|||
<div class="log-description">{log.description}</div> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
div.task { |
|||
display: flex; |
|||
flex-direction: row; |
|||
margin: 0.25em 0 0.75em 0; |
|||
} |
|||
div.icon { |
|||
display: flex; |
|||
flex-direction: column; |
|||
font-size: 1em; |
|||
padding: 0.125em .5ch; |
|||
|
|||
margin-right: 0.5em; |
|||
background: #444; |
|||
color: #CCC; |
|||
} |
|||
div.icon.done { |
|||
padding-top: 0.2em; |
|||
background: #484; |
|||
color: #78ff78; |
|||
} |
|||
div.body { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
} |
|||
div.header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
background: #333; |
|||
} |
|||
|
|||
div.name { |
|||
font-size: 1em; |
|||
font-weight: 100; |
|||
margin: auto 0; |
|||
vertical-align: middle; |
|||
padding: 0.125em .5ch; |
|||
} |
|||
div.times { |
|||
margin-left: auto; |
|||
margin-right: 0.25ch; |
|||
padding: 0.125em 0; |
|||
} |
|||
|
|||
div.description { |
|||
padding: 0.25em 1ch; |
|||
background: #222; |
|||
color: #aaa; |
|||
border-bottom-right-radius: 0.5em; |
|||
} |
|||
div.description p { |
|||
padding: 0; |
|||
margin: 0.25em 0; |
|||
} |
|||
|
|||
div.log-list { |
|||
padding: 0.5em 0; |
|||
} |
|||
div.log { |
|||
padding: 0.25em 1ch; |
|||
} |
|||
div.log-time { |
|||
font-size: 0.75em; |
|||
font-weight: 800; |
|||
} |
|||
|
|||
div.item { |
|||
display: flex; |
|||
flex-direction: row; |
|||
margin-top: 0.25em; |
|||
margin-bottom: 0em; |
|||
font-size: 0.75em; |
|||
} |
|||
div.item div.item-icon { |
|||
padding: 0.25em 0.5ch 0.25em 0; |
|||
} |
|||
div.item div.item-name { |
|||
padding: 0.125em; |
|||
} |
|||
</style> |
@ -0,0 +1,138 @@ |
|||
import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion"; |
|||
import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus"; |
|||
import { faCube } from "@fortawesome/free-solid-svg-icons/faCube"; |
|||
import { faCubes } from "@fortawesome/free-solid-svg-icons/faCubes"; |
|||
import { faBook } from "@fortawesome/free-solid-svg-icons/faBook"; |
|||
import { faBookOpen } from "@fortawesome/free-solid-svg-icons/faBookOpen"; |
|||
import { faBookDead } from "@fortawesome/free-solid-svg-icons/faBookDead"; |
|||
import { faPen } from "@fortawesome/free-solid-svg-icons/faPen"; |
|||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons/faPencilAlt"; |
|||
import { faDiceD20 } from "@fortawesome/free-solid-svg-icons/faDiceD20"; |
|||
import { faDiceD6 } from "@fortawesome/free-solid-svg-icons/faDiceD6"; |
|||
import { faDungeon } from "@fortawesome/free-solid-svg-icons/faDungeon"; |
|||
import { faGamepad } from "@fortawesome/free-solid-svg-icons/faGamepad"; |
|||
import { faHeadphones } from "@fortawesome/free-solid-svg-icons/faHeadphones"; |
|||
import { faLanguage } from "@fortawesome/free-solid-svg-icons/faLanguage"; |
|||
import { faCode } from "@fortawesome/free-solid-svg-icons/faCode"; |
|||
import { faCodeBranch } from "@fortawesome/free-solid-svg-icons/faCodeBranch"; |
|||
import { faGuitar } from "@fortawesome/free-solid-svg-icons/faGuitar"; |
|||
import { faMusic } from "@fortawesome/free-solid-svg-icons/faMusic"; |
|||
import { faArchive } from "@fortawesome/free-solid-svg-icons/faArchive"; |
|||
import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck"; |
|||
import { faDrawPolygon } from "@fortawesome/free-solid-svg-icons/faDrawPolygon"; |
|||
import { faComment } from "@fortawesome/free-solid-svg-icons/faComment"; |
|||
import { faDatabase } from "@fortawesome/free-solid-svg-icons/faDatabase"; |
|||
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog"; |
|||
import { faLink } from "@fortawesome/free-solid-svg-icons/faLink"; |
|||
import { faStar } from "@fortawesome/free-solid-svg-icons/faStar"; |
|||
import { faStarOfLife } from "@fortawesome/free-solid-svg-icons/faStarOfLife"; |
|||
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun"; |
|||
import { faHdd } from "@fortawesome/free-solid-svg-icons/faHdd"; |
|||
import { faServer } from "@fortawesome/free-solid-svg-icons/faServer"; |
|||
import { faBlender } from "@fortawesome/free-solid-svg-icons/faBlender"; |
|||
import { faCross } from "@fortawesome/free-solid-svg-icons/faCross"; |
|||
import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes"; |
|||
import { faSkullCrossbones } from "@fortawesome/free-solid-svg-icons/faSkullCrossbones"; |
|||
import { faCrosshairs } from "@fortawesome/free-solid-svg-icons/faCrosshairs"; |
|||
import { faLaptop } from "@fortawesome/free-solid-svg-icons/faLaptop"; |
|||
import { faMemory } from "@fortawesome/free-solid-svg-icons/faMemory"; |
|||
import { faKeyboard } from "@fortawesome/free-solid-svg-icons/faKeyboard"; |
|||
import { faCookie } from "@fortawesome/free-solid-svg-icons/faCookie"; |
|||
import { faMicrochip } from "@fortawesome/free-solid-svg-icons/faMicrochip"; |
|||
import { faClipboard } from "@fortawesome/free-solid-svg-icons/faClipboard"; |
|||
import { faPizzaSlice } from "@fortawesome/free-solid-svg-icons/faPizzaSlice"; |
|||
import { faPaperclip } from "@fortawesome/free-solid-svg-icons/faPaperclip"; |
|||
import { faReceipt } from "@fortawesome/free-solid-svg-icons/faReceipt"; |
|||
import { faSuperscript } from "@fortawesome/free-solid-svg-icons/faSuperscript"; |
|||
import { faCouch } from "@fortawesome/free-solid-svg-icons/faCouch"; |
|||
import { faTerminal } from "@fortawesome/free-solid-svg-icons/faTerminal"; |
|||
import { faGift } from "@fortawesome/free-solid-svg-icons/faGift"; |
|||
import { faGifts } from "@fortawesome/free-solid-svg-icons/faGifts"; |
|||
import { faImage } from "@fortawesome/free-solid-svg-icons/faImage"; |
|||
import { faImages } from "@fortawesome/free-solid-svg-icons/faImages"; |
|||
import { faDragon } from "@fortawesome/free-solid-svg-icons/faDragon"; |
|||
import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb"; |
|||
import { faTools } from "@fortawesome/free-solid-svg-icons/faTools"; |
|||
import { faHammer } from "@fortawesome/free-solid-svg-icons/faHammer"; |
|||
import { faScrewdriver } from "@fortawesome/free-solid-svg-icons/faScrewdriver"; |
|||
import { faWrench } from "@fortawesome/free-solid-svg-icons/faWrench"; |
|||
import { faBug } from "@fortawesome/free-solid-svg-icons/faBug"; |
|||
import { faUtensils } from "@fortawesome/free-solid-svg-icons/faUtensils"; |
|||
import { faHome } from "@fortawesome/free-solid-svg-icons/faHome"; |
|||
import { faIgloo } from "@fortawesome/free-solid-svg-icons/faIgloo"; |
|||
import { faWarehouse } from "@fortawesome/free-solid-svg-icons/faWarehouse"; |
|||
import { faToiletPaperSlash } from "@fortawesome/free-solid-svg-icons/faToiletPaperSlash"; |
|||
|
|||
const icons = { |
|||
"question": faQuestion, |
|||
"plus": faPlus, |
|||
"cube": faCube, |
|||
"cubes": faCubes, |
|||
"book": faBook, |
|||
"book_open": faBookOpen, |
|||
"book_dead": faBookDead, |
|||
"pen": faPen, |
|||
"pencil_alt": faPencilAlt, |
|||
"draw_poligon": faDrawPolygon, |
|||
"dice_d20": faDiceD20, |
|||
"dice_d6": faDiceD6, |
|||
"dungeon": faDungeon, |
|||
"gamepad": faGamepad, |
|||
"headphones": faHeadphones, |
|||
"language": faLanguage, |
|||
"code": faCode, |
|||
"code_branch": faCodeBranch, |
|||
"guitar": faGuitar, |
|||
"archive": faArchive, |
|||
"check": faCheck, |
|||
"music": faMusic, |
|||
"comment": faComment, |
|||
"database": faDatabase, |
|||
"cog": faCog, |
|||
"link": faLink, |
|||
"star": faStar, |
|||
"star_of_life": faStarOfLife, |
|||
"sun": faSun, |
|||
"hdd": faHdd, |
|||
"server": faServer, |
|||
"blender": faBlender, |
|||
"cross": faCross, |
|||
"times": faTimes, |
|||
"crosshairs": faCrosshairs, |
|||
"skull_crossbones": faSkullCrossbones, |
|||
"laptop": faLaptop, |
|||
"memory": faMemory, |
|||
"keyboard": faKeyboard, |
|||
"cookie": faCookie, |
|||
"microchip": faMicrochip, |
|||
"clipboard": faClipboard, |
|||
"pizza_slice": faPizzaSlice, |
|||
"paperclip": faPaperclip, |
|||
"receipt": faReceipt, |
|||
"superscript": faSuperscript, |
|||
"couch": faCouch, |
|||
"terminal": faTerminal, |
|||
"gift": faGift, |
|||
"gifts": faGifts, |
|||
"image": faImage, |
|||
"images": faImages, |
|||
"dragon": faDragon, |
|||
"lightbulb": faLightbulb, |
|||
"tools": faTools, |
|||
"hammer": faHammer, |
|||
"screwdriver": faScrewdriver, |
|||
"wrench": faWrench, |
|||
"bug": faBug, |
|||
"utensils": faUtensils, |
|||
"home": faHome, |
|||
"igloo": faIgloo, |
|||
"warehouse": faWarehouse, |
|||
"toilet_paper_slash": faToiletPaperSlash, |
|||
}; |
|||
|
|||
export type IconName = keyof typeof icons; |
|||
|
|||
export const iconNames = Object.keys(icons).sort() as IconName[]; |
|||
export const DEFAULT_ICON = iconNames[0] as IconName; |
|||
|
|||
export default icons; |
@ -0,0 +1,112 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import modalStore from "../stores/modal"; |
|||
import goalStore, { fpGoalStore } from "../stores/goal"; |
|||
import groupStore from "../stores/goal"; |
|||
import IconSelect from "../components/IconSelect.svelte"; |
|||
import { DEFAULT_ICON } from "../external/icons"; |
|||
import type { IconName } from "../external/icons"; |
|||
import type { GoalResult } from "../models/goal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
import { formatFormTime, nextMonth } from "../utils/time"; |
|||
import GroupSelect from "../components/GroupSelect.svelte"; |
|||
|
|||
export let deletion = false; |
|||
export let creation = false; |
|||
|
|||
const md = $modalStore; |
|||
|
|||
let goal: GoalResult = { |
|||
id: "", |
|||
groupId: "", |
|||
startTime: nextMonth(new Date()).toISOString(), |
|||
endTime: new Date(nextMonth(nextMonth(new Date())).getTime() - 1).toISOString(), |
|||
amount: 1, |
|||
name: "", |
|||
description: "", |
|||
completedAmount: 0, |
|||
group: {id: "", name: "", icon: "question", description: ""}, |
|||
items: [], |
|||
logs: [], |
|||
}; |
|||
let verb = "Add"; |
|||
if (md.name === "goal.edit" || md.name === "goal.delete") { |
|||
goal = md.goal; |
|||
verb = (md.name === "goal.edit") ? "Edit" : "Delete"; |
|||
} else if (md.name !== "goal.add") { |
|||
throw new Error("Wrong form") |
|||
} |
|||
|
|||
let name = goal.name; |
|||
let description = goal.description; |
|||
let groupId = goal.groupId; |
|||
let amount = goal.amount; |
|||
let startTime = formatFormTime(goal.startTime); |
|||
let endTime = formatFormTime(goal.endTime); |
|||
|
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
if (creation) { |
|||
stuffLogClient.createGoal({ |
|||
startTime: new Date(startTime), |
|||
endTime: new Date(endTime), |
|||
groupId, name, description, amount, |
|||
}).then(() => { |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
} else if (deletion) { |
|||
stuffLogClient.deleteGoal(goal.id).then(() => { |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
} else { |
|||
stuffLogClient.updateGoal(goal.id, { |
|||
startTime: new Date(startTime), |
|||
endTime: new Date(endTime), |
|||
name, description, amount, |
|||
}).then(() => { |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
} |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="{verb} Goal" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="name">Name</label> |
|||
<input disabled={deletion} name="name" type="text" bind:value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea disabled={deletion} name="description" bind:value={description} /> |
|||
<label for="groupId">Group</label> |
|||
<GroupSelect disabled={!creation} name="groupId" bind:value={groupId}/> |
|||
<label for="amount">Amount</label> |
|||
<input disabled={deletion} name="amount" type="number" bind:value={amount} /> |
|||
<label for="startTime">Start Time</label> |
|||
<input disabled={deletion} name="startTime" type="datetime-local" bind:value={startTime} /> |
|||
<label for="endTime">End Time</label> |
|||
<input disabled={deletion} name="endTime" type="datetime-local" bind:value={endTime} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">{verb} Goal</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,90 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import modalStore from "../stores/modal"; |
|||
import goalStore, { fpGoalStore } from "../stores/goal"; |
|||
import groupStore from "../stores/group"; |
|||
import IconSelect from "../components/IconSelect.svelte"; |
|||
import { DEFAULT_ICON } from "../external/icons"; |
|||
import type { IconName } from "../external/icons"; |
|||
import type { GroupResult } from "../models/group"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
|
|||
export let deletion = false; |
|||
export let creation = false; |
|||
|
|||
const md = $modalStore; |
|||
let group: GroupResult = { |
|||
id: "", |
|||
name: "", |
|||
description: "", |
|||
icon: DEFAULT_ICON, |
|||
items: [], |
|||
}; |
|||
let verb = "Add"; |
|||
if (md.name === "group.edit" || md.name === "group.delete") { |
|||
group = md.group; |
|||
verb = (md.name === "group.edit") ? "Edit" : "Delete"; |
|||
} else if (md.name !== "group.add") { |
|||
throw new Error("Wrong form") |
|||
} |
|||
|
|||
let name = group.name; |
|||
let description = group.description; |
|||
let icon = group.icon as IconName; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
if (creation) { |
|||
stuffLogClient.createGroup({ |
|||
name, description, icon, |
|||
}).then(() => { |
|||
groupStore.markStale(); |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
} else if (deletion) { |
|||
stuffLogClient.deleteGroup(group.id).then(() => { |
|||
groupStore.markStale(); |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
} else { |
|||
stuffLogClient.updateGroup(group.id, { |
|||
name, description, icon, |
|||
}).then(() => { |
|||
groupStore.markStale(); |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
projectStore.markStale(); |
|||
fpProjectStore.markStale(); |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
} |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="{verb} Group" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="name">Name</label> |
|||
<input disabled={deletion} name="name" type="text" bind:value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea disabled={deletion} name="description" bind:value={description} /> |
|||
<label for="icon">Icon</label> |
|||
<IconSelect disabled={deletion} bind:value={icon} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">{verb} Group</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,54 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import modalStore from "../stores/modal"; |
|||
import goalStore, { fpGoalStore } from "../stores/goal"; |
|||
import groupStore from "../stores/group"; |
|||
|
|||
const md = $modalStore; |
|||
if (md.name !== "item.add") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
let group = md.group; |
|||
let name = ""; |
|||
let description = ""; |
|||
let groupWeight = 1; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
stuffLogClient.createItem({ |
|||
groupId: group.id, |
|||
|
|||
name, description, groupWeight, |
|||
}).then(() => { |
|||
groupStore.markStale(); |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
modalStore.close(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Add Item" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="groupName">Group</label> |
|||
<input disabled name="groupName" type="text" value={group.name} /> |
|||
<label for="name">Name</label> |
|||
<input name="name" type="text" bind:value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={description} /> |
|||
<label for="groupWeight">Group Weight</label> |
|||
<input name="groupWeight" type="number" bind:value={groupWeight} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Add Item</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,50 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import modalStore from "../stores/modal"; |
|||
import groupStore from "../stores/group"; |
|||
|
|||
const md = $modalStore; |
|||
if (md.name !== "item.delete") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
let item = md.item; |
|||
let group = md.group; |
|||
let name = item.name; |
|||
let description = item.description; |
|||
let groupWeight = item.groupWeight; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
stuffLogClient.deleteItem(item.id).then(() => { |
|||
groupStore.markStale(); |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Edit Item" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="groupName">Group</label> |
|||
<input disabled name="groupName" type="text" value={group.name} /> |
|||
<label for="name">Name</label> |
|||
<input disabled name="name" type="text" value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea disabled name="description" value={description} /> |
|||
<label for="groupWeight">Group Weight</label> |
|||
<input disabled name="groupWeight" type="number" value={groupWeight} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Delete Item</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,56 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import modalStore from "../stores/modal"; |
|||
import goalStore, { fpGoalStore } from "../stores/goal"; |
|||
import groupStore from "../stores/group"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
|
|||
const md = $modalStore; |
|||
if (md.name !== "item.edit") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
let item = md.item; |
|||
let group = md.group; |
|||
let name = item.name; |
|||
let description = item.description; |
|||
let groupWeight = item.groupWeight; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
stuffLogClient.updateItem(item.id, { |
|||
name, description, groupWeight, |
|||
}).then(() => { |
|||
groupStore.markStale(); |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
projectStore.markStale(); |
|||
fpProjectStore.markStale(); |
|||
modalStore.close(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Edit Item" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="groupName">Group</label> |
|||
<input disabled name="groupName" type="text" value={group.name} /> |
|||
<label for="name">Name</label> |
|||
<input name="name" type="text" bind:value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={description} /> |
|||
<label for="groupWeight">Group Weight</label> |
|||
<input name="groupWeight" type="number" bind:value={groupWeight} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Edit Item</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,71 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import goalStore, { fpGoalStore } from "../stores/goal"; |
|||
import logStore from "../stores/logs"; |
|||
import modalStore from "../stores/modal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
import { formatFormTime } from "../utils/time"; |
|||
|
|||
let loggedTime = formatFormTime(new Date); |
|||
let taskName = ""; |
|||
let description = ""; |
|||
let markInactive = false; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
const md = $modalStore; |
|||
if (md.name !== "log.add") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
stuffLogClient.createLog({ |
|||
taskId: md.task.id, |
|||
loggedTime: new Date(loggedTime).toISOString(), |
|||
description, |
|||
}).then(() => { |
|||
if (markInactive) { |
|||
return stuffLogClient.updateTask(md.task.id, {active: false}) |
|||
} |
|||
}).then(() => { |
|||
modalStore.close(); |
|||
}).finally(() => { |
|||
projectStore.markStale(); |
|||
fpProjectStore.markStale(); |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
logStore.markStale(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
|
|||
$: { |
|||
const md = $modalStore; |
|||
if (md.name === "log.add") { |
|||
taskName = md.task.name; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Add Log" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="taskName">Task</label> |
|||
<input disabled name="taskName" type="text" bind:value={taskName} /> |
|||
|
|||
<label for="loggedTime">Logged Time</label> |
|||
<input name="loggedTime" type="datetime-local" bind:value={loggedTime} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={description} /> |
|||
<input id="markInactive" type="checkbox" bind:checked={markInactive} /> |
|||
<label for="markInactive">Complete Task</label> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Add Log</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,56 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import modalStore from "../stores/modal"; |
|||
import goalStore, { fpGoalStore } from "../stores/goal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
import { formatFormTime } from "../utils/time"; |
|||
import logStore from "../stores/logs"; |
|||
|
|||
const md = $modalStore; |
|||
if (md.name !== "log.delete") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
let loggedTime = formatFormTime(new Date(md.log.loggedTime)); |
|||
let description = md.log.description; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
const md = $modalStore; |
|||
if (md.name !== "log.delete") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
stuffLogClient.deleteLog(md.log.id).then(() => { |
|||
projectStore.markStale(); |
|||
fpProjectStore.markStale(); |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
logStore.markStale(); |
|||
|
|||
modalStore.close(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Delete Log" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="taskName">Task</label> |
|||
<input disabled name="taskName" type="text" bind:value={md.log.task.name} /> |
|||
<label for="loggedTime">Logged Time</label> |
|||
<input disabled name="loggedTime" type="datetime-local" bind:value={loggedTime} /> |
|||
<label for="description">Description</label> |
|||
<textarea disabled name="description" bind:value={description} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Delete Log</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,59 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import modalStore from "../stores/modal"; |
|||
import goalStore, { fpGoalStore } from "../stores/goal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
import { formatFormTime } from "../utils/time"; |
|||
import logStore from "../stores/logs"; |
|||
|
|||
const md = $modalStore; |
|||
if (md.name !== "log.edit") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
let loggedTime = formatFormTime(new Date(md.log.loggedTime)); |
|||
let description = md.log.description; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
const md = $modalStore; |
|||
if (md.name !== "log.edit") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
stuffLogClient.updateLog(md.log.id, { |
|||
loggedTime: new Date(loggedTime).toISOString(), |
|||
description, |
|||
}).then(() => { |
|||
projectStore.markStale(); |
|||
fpProjectStore.markStale(); |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
logStore.markStale(); |
|||
|
|||
modalStore.close(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Edit Log" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="taskName">Task</label> |
|||
<input disabled name="taskName" type="text" bind:value={md.log.task.name} /> |
|||
<label for="loggedTime">Logged Time</label> |
|||
<input name="loggedTime" type="datetime-local" bind:value={loggedTime} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={description} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Edit Log</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,74 @@ |
|||
<script lang="ts"> |
|||
import type {CognitoUser} from "amazon-cognito-identity-js"; |
|||
|
|||
import { signIn } from "../clients/amplify"; |
|||
import authStore from "../stores/auth"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
|
|||
let user: CognitoUser | null = null; |
|||
let username = ""; |
|||
let password = ""; |
|||
let newPassword = ""; |
|||
let newPasswordRepeat = ""; |
|||
let settingNewPassword = false; |
|||
let error = null; |
|||
let done = false; |
|||
|
|||
function login() { |
|||
error = null; |
|||
|
|||
if (settingNewPassword) { |
|||
if (newPasswordRepeat !== newPassword) { |
|||
error = "New passwords do not match."; |
|||
return; |
|||
} |
|||
|
|||
user.completeNewPasswordChallenge(newPassword, null, { |
|||
onSuccess: () => { |
|||
done = true; |
|||
authStore.check(); |
|||
}, |
|||
onFailure: err => { |
|||
error = err |
|||
}, |
|||
}) |
|||
} else { |
|||
signIn(username, password).then(newUser => { |
|||
if (!newUser) { |
|||
error = "Incorrect username or password." |
|||
return |
|||
} |
|||
|
|||
if ((newUser as any).challengeName === "NEW_PASSWORD_REQUIRED") { |
|||
error = "Password is expired, please update it." |
|||
settingNewPassword = true; |
|||
} else { |
|||
authStore.check(); |
|||
done = true; |
|||
} |
|||
|
|||
user = newUser; |
|||
}).catch(err => { |
|||
error = err |
|||
}); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Modal show={!done} title="Login" error={error}> |
|||
<form on:submit|preventDefault={login}> |
|||
<label for="username">Username</label> |
|||
<input name="username" type="text" bind:value={username} /> |
|||
<label for="password">Password</label> |
|||
<input name="password" type="password" bind:value={password} /> |
|||
{#if settingNewPassword} |
|||
<label for="newPassword">New Password</label> |
|||
<input name="newPassword" type="password" bind:value={newPassword} /> |
|||
<label for="newPasswordRepeat">New Password</label> |
|||
<input name="newPasswordRepeat" type="password" bind:value={newPasswordRepeat} /> |
|||
{/if} |
|||
|
|||
<hr /> |
|||
<button type="submit">Login</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,54 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import IconSelect from "../components/IconSelect.svelte"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import { iconNames } from "../external/icons"; |
|||
import modalStore from "../stores/modal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
|
|||
let endTime = ""; |
|||
let name = ""; |
|||
let description = ""; |
|||
let icon = iconNames[0]; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
stuffLogClient.createProject({ |
|||
active: true, |
|||
endTime: ( endTime == "" ) ? null : new Date(endTime), |
|||
|
|||
name, description, icon, |
|||
}).then(() => { |
|||
projectStore.markStale(); |
|||
if (endTime !== "") { |
|||
fpProjectStore.markStale(); |
|||
} |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Add Project" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="name">Name</label> |
|||
<input name="name" type="text" bind:value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={description} /> |
|||
<label for="itemId">Icon</label> |
|||
<IconSelect bind:value={icon} /> |
|||
<label for="endTime">Deadline (Optional)</label> |
|||
<input name="endTime" type="datetime-local" bind:value={endTime} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Add Project</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,56 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import IconSelect from "../components/IconSelect.svelte"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import type { IconName } from "../external/icons"; |
|||
import modalStore from "../stores/modal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
import { formatFormTime } from "../utils/time"; |
|||
|
|||
const md = $modalStore; |
|||
if (md.name !== "project.delete") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
const project = md.project; |
|||
|
|||
let endTime = project.endTime ? formatFormTime(project.endTime) : ""; |
|||
let name = project.name; |
|||
let description = project.description; |
|||
let icon = project.icon as IconName; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
stuffLogClient.deleteProject(project.id).then(() => { |
|||
projectStore.markStale(); |
|||
if (endTime !== "") { |
|||
fpProjectStore.markStale(); |
|||
} |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Delete Project" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="name">Name</label> |
|||
<input disabled name="name" type="text" value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea disabled name="description" value={description} /> |
|||
<label for="itemId">Icon</label> |
|||
<IconSelect disabled value={icon} /> |
|||
<label for="endTime">Deadline (Optional)</label> |
|||
<input disabled name="endTime" type="datetime-local" value={endTime} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Delete Project</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,60 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import IconSelect from "../components/IconSelect.svelte"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import type { IconName } from "../external/icons"; |
|||
import modalStore from "../stores/modal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
import { formatFormTime } from "../utils/time"; |
|||
|
|||
const md = $modalStore; |
|||
if (md.name !== "project.edit") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
const project = md.project; |
|||
|
|||
let endTime = project.endTime ? formatFormTime(project.endTime) : ""; |
|||
let name = project.name; |
|||
let description = project.description; |
|||
let icon = project.icon as IconName; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
stuffLogClient.updateProject(project.id, { |
|||
active: true, |
|||
endTime: ( endTime == "" ) ? null : new Date(endTime), |
|||
clearEndTime: ( endTime == "" ), |
|||
|
|||
name, description, icon, |
|||
}).then(() => { |
|||
projectStore.markStale(); |
|||
fpProjectStore.markStale(); |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Edit Project" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="name">Name</label> |
|||
<input name="name" type="text" bind:value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={description} /> |
|||
<label for="itemId">Icon</label> |
|||
<IconSelect bind:value={icon} /> |
|||
<label for="endTime">Deadline (Optional)</label> |
|||
<input name="endTime" type="datetime-local" bind:value={endTime} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Edit Project</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,72 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import ItemSelect from "../components/ItemSelect.svelte"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import type { ProjectResult } from "../models/project"; |
|||
import modalStore from "../stores/modal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
|
|||
let project: ProjectResult |
|||
let endTime = ""; |
|||
let itemId = ""; |
|||
let name = ""; |
|||
let description = ""; |
|||
let itemAmount = 1; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
stuffLogClient.createTask({ |
|||
projectId: project.id, |
|||
itemId: itemId, |
|||
active: true, |
|||
endTime: ( endTime == "") ? null : new Date(endTime), |
|||
|
|||
name, description, itemAmount, |
|||
}).then(() => { |
|||
projectStore.markStale(); |
|||
fpProjectStore.markStale(); |
|||
modalStore.close(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
|
|||
$: { |
|||
const md = $modalStore; |
|||
if (md.name !== "task.add") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
if (itemId === "") { |
|||
project = md.project; |
|||
if (project.tasks.length > 0) { |
|||
itemId = project.tasks[0].itemId; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Add Task" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="projectName">Project</label> |
|||
<input disabled name="projectName" type="text" value={project.name} /> |
|||
<label for="name">Name</label> |
|||
<input name="name" type="text" bind:value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={description} /> |
|||
<label for="itemId">Item {itemId}</label> |
|||
<ItemSelect name="itemId" bind:value={itemId} /> |
|||
<label for="itemAmount">Amount</label> |
|||
<input name="itemAmount" type="number" bind:value={itemAmount} /> |
|||
<label for="endTime">Deadline (Optional)</label> |
|||
<input name="endTime" type="datetime-local" bind:value={endTime} /> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Add Task</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,60 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import goalStore, { fpGoalStore } from "../stores/goal"; |
|||
import modalStore from "../stores/modal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
import { formatFormTime } from "../utils/time"; |
|||
|
|||
const md = $modalStore; |
|||
if (md.name !== "task.delete") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
let task = md.task |
|||
let name = task.name; |
|||
let description = task.description; |
|||
let itemAmount = task.itemAmount; |
|||
let active = task.active; |
|||
let endTime = task.endTime ? formatFormTime(task.endTime) : ""; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
stuffLogClient.deleteTask(task.id).then(() => { |
|||
projectStore.markStale(); |
|||
fpProjectStore.markStale(); |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
modalStore.close(); |
|||
}).catch(err => { |
|||
error = err.message ? err.message : err.toString(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Delete Task" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="name">Name</label> |
|||
<input disabled name="name" type="text" value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea disabled name="description" value={description} /> |
|||
<label for="name">Item</label> |
|||
<input disabled name="name" type="text" value={task.item.name} /> |
|||
<label for="itemAmount">Amount</label> |
|||
<input disabled name="itemAmount" type="number" value={itemAmount} /> |
|||
<label for="endTime">Deadline (Optional)</label> |
|||
<input disabled name="endTime" type="datetime-local" value={endTime} /> |
|||
<input id="active" type="checkbox" checked={active} /> |
|||
<label for="active">Task is active/incomplete</label> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Delete1 Task</button> |
|||
</form> |
|||
</Modal> |
@ -0,0 +1,63 @@ |
|||
<script lang="ts"> |
|||
import stuffLogClient from "../clients/stufflog"; |
|||
import Modal from "../components/Modal.svelte"; |
|||
import goalStore, { fpGoalStore } from "../stores/goal"; |
|||
import modalStore from "../stores/modal"; |
|||
import projectStore, { fpProjectStore } from "../stores/project"; |
|||
import { formatFormTime } from "../utils/time"; |
|||
|
|||
const md = $modalStore; |
|||
if (md.name !== "task.edit") { |
|||
throw new Error("Wrong form"); |
|||
} |
|||
|
|||
let task = md.task |
|||
let name = task.name; |
|||
let description = task.description; |
|||
let itemAmount = task.itemAmount; |
|||
let active = task.active; |
|||
let endTime = task.endTime ? formatFormTime(task.endTime) : ""; |
|||
let error = null; |
|||
|
|||
function onSubmit() { |
|||
stuffLogClient.updateTask(task.id, { |
|||
endTime: (endTime == "") ? null : new Date(endTime), |
|||
clearEndTime: endTime == "", |
|||
|
|||
name, description, itemAmount, active, |
|||
}).then(() => { |
|||
projectStore.markStale(); |
|||
fpProjectStore.markStale(); |
|||
goalStore.markStale(); |
|||
fpGoalStore.markStale(); |
|||
modalStore.close(); |
|||
}) |
|||
|
|||
error = null; |
|||
} |
|||
|
|||
function onClose() { |
|||
modalStore.close(); |
|||
} |
|||
</script> |
|||
|
|||
<Modal show title="Add Task" error={error} closable on:close={onClose}> |
|||
<form on:submit|preventDefault={onSubmit}> |
|||
<label for="name">Name</label> |
|||
<input name="name" type="text" bind:value={name} /> |
|||
<label for="description">Description</label> |
|||
<textarea name="description" bind:value={description} /> |
|||
<label for="name">Item</label> |
|||
<input disabled name="name" type="text" value={task.item.name} /> |
|||
<label for="itemAmount">Amount</label> |
|||
<input name="itemAmount" type="number" bind:value={itemAmount} /> |
|||
<label for="endTime">Deadline (Optional)</label> |
|||
<input name="endTime" type="datetime-local" bind:value={endTime} /> |
|||
<input id="active" type="checkbox" bind:checked={active} /> |
|||
<label for="active">Task is active/incomplete</label> |
|||
|
|||
<hr /> |
|||
|
|||
<button type="submit">Add Task</button> |
|||
</form> |
|||
</Modal> |
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue