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