From 4c02dbb1d4790858e73ad3a5705e467bb6bb5dc7 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 14 Jul 2023 16:05:57 +0100 Subject: [PATCH] Add tags --- .editorconfig | 2 +- client/src/lib/Counter.svelte | 10 --- client/src/lib/Schedule.svelte | 33 +++++++-- client/src/lib/api.ts | 19 +++-- controller.go | 75 ++++++++++++++++++- main.go | 130 +++++++++++++++++++++++++++------ public/index.html | 16 ---- 7 files changed, 221 insertions(+), 64 deletions(-) delete mode 100644 client/src/lib/Counter.svelte delete mode 100644 public/index.html diff --git a/.editorconfig b/.editorconfig index f48caa3..d5f6fd4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,4 +7,4 @@ indent_style = space indent_size = 4 [*.{yaml,yml}] -indent_size = 2 \ No newline at end of file +indent_size = 2 diff --git a/client/src/lib/Counter.svelte b/client/src/lib/Counter.svelte deleted file mode 100644 index 979b4df..0000000 --- a/client/src/lib/Counter.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/client/src/lib/Schedule.svelte b/client/src/lib/Schedule.svelte index eb55818..afd3dc9 100644 --- a/client/src/lib/Schedule.svelte +++ b/client/src/lib/Schedule.svelte @@ -20,13 +20,20 @@ let newItemName = ""; let newItemDate = (new Date()).toISOString().split("T")[0]; let newItemDuration = "1h"; + let newItemRawTags = ""; + $: newItemTags = newItemRawTags.split(";").filter(x => x); function createItem() { $createMutation.mutate({ name: newItemName, date: newItemDate, - duration: newItemDuration + duration: newItemDuration, + tags: newItemTags }); + + newItemName = ""; + newItemDuration = "1h"; + document.getElementById("name-input").focus(); } @@ -62,7 +69,12 @@
{#each items as item}
- {item.name} + + {item.name} + {#each item.tags as tag} + {tag.name} + {/each} +
- +

New

@@ -95,15 +107,24 @@
+
+ + +
+ {#each newItemTags as tag} + {tag} + {/each} +
+
- +
- +

Raw Data

{#if $queryResult.data}
{JSON.stringify($queryResult.data, null, 2)}
{/if} - \ No newline at end of file + diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index d3e1d82..6385368 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -1,6 +1,13 @@ const SCHEDULE_DAYS = 14; const API_URL = import.meta.env.PROD ? "/api" : import.meta.env.VITE_API_URL; +type ScheduleTag = { + id: number, + createdAt: string, + updatedAt: string, + name: string +} + type ScheduleItem = { id: number, createdAt: string, @@ -9,12 +16,14 @@ type ScheduleItem = { description: string | null, duration: string, date: string + tags: ScheduleTag[] }; + type Schedule = { [date: string]: ScheduleItem[] }; export async function getSchedule(): Promise { const res = await fetch(`${API_URL}/schedule?days=${SCHEDULE_DAYS}`); - + if (res?.ok) { return await res.json(); } else { @@ -22,7 +31,7 @@ export async function getSchedule(): Promise { } } -export async function createScheduleItem(data: { name: string, duration: string, date: string }) { +export async function createScheduleItem(data: { name: string, duration: string, date: string, tags: string[] }) { const res = await fetch(`${API_URL}/items`, { method: "POST", headers: { @@ -33,7 +42,7 @@ export async function createScheduleItem(data: { name: string, duration: string, date: new Date(data.date).toISOString() }) }); - + if (res?.ok) { return await res.json(); } else { @@ -45,10 +54,10 @@ export async function deleteScheduleItem(id: number) { const res = await fetch(`${API_URL}/items/${id}`, { method: "DELETE" }); - + if (res?.ok) { return; } else { throw new Error(`Invalid response code ${res?.status}`); } -} \ No newline at end of file +} diff --git a/controller.go b/controller.go index c7efe2b..91f86aa 100644 --- a/controller.go +++ b/controller.go @@ -4,17 +4,18 @@ import ( "time" "gorm.io/gorm" + "gorm.io/gorm/clause" ) func GetScheduleItems(db *gorm.DB, fromDate time.Time, toDate time.Time) ([]ScheduleItem, error) { var items []ScheduleItem - res := db.Where("date BETWEEN ? AND ?", fromDate.Format(time.DateOnly), toDate.Format(time.DateOnly)).Find(&items) + res := db.Preload("Tags").Where("date BETWEEN ? AND ?", fromDate.Format(time.DateOnly), toDate.Format(time.DateOnly)).Find(&items) return items, res.Error } func GetScheduleItem(db *gorm.DB, id int) (*ScheduleItem, error) { var item ScheduleItem - res := db.First(&item, id) + res := db.Preload("Tags").First(&item, id) if res.RowsAffected < 1 { return nil, nil @@ -37,15 +38,83 @@ func DeleteScheduleItem(db *gorm.DB, id int) (bool, error) { return true, nil } -func CreateScheduleItem(db *gorm.DB, name string, description *string, duration Duration, date time.Time) (ScheduleItem, error) { +func CreateScheduleItem(db *gorm.DB, name string, description *string, duration Duration, date time.Time, tagNames []string) (ScheduleItem, error) { + var tags = []ScheduleTag{} + var err error + + if len(tagNames) > 0 { + tags, err = UpsertScheduleTags(db, tagNames) + if err != nil { + return ScheduleItem{}, err + } + } + item := ScheduleItem{ Name: name, Description: description, Duration: duration, Date: date, + Tags: tags, } res := db.Create(&item) return item, res.Error } + +func GetScheduleTags(db *gorm.DB) ([]ScheduleTag, error) { + var tags []ScheduleTag + res := db.Find(&tags) + return tags, res.Error +} + +func GetScheduleTag(db *gorm.DB, id int) (*ScheduleTag, error) { + var tag ScheduleTag + res := db.First(&tag, id) + + if res.RowsAffected < 1 { + return nil, nil + } + + return &tag, res.Error +} + +func DeleteScheduleTag(db *gorm.DB, id int) (bool, error) { + res := db.Delete(&ScheduleTag{}, id) + + if res.Error != nil { + return false, res.Error + } + + if res.RowsAffected < 1 { + return false, nil + } + + return true, nil +} + +func CreateScheduleTag(db *gorm.DB, name string) (ScheduleTag, error) { + tag := ScheduleTag{ + Name: name, + } + + res := db.Create(&tag) + + return tag, res.Error +} + +func UpsertScheduleTags(db *gorm.DB, names []string) ([]ScheduleTag, error) { + var tags = []ScheduleTag{} + + for _, name := range names { + tags = append(tags, ScheduleTag{Name: name}) + } + + res := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&tags) + return tags, res.Error +} + +func Cleanup(db *gorm.DB) error { + res := db.Exec("select * from schedule_tags where id not in (select schedule_tag_id from schedule_item_tags);") + return res.Error +} diff --git a/main.go b/main.go index f08a638..d0bd970 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,16 @@ package main import ( - "embed" - "github.com/gofiber/fiber/v2/middleware/filesystem" - "log" + "embed" + "log" "net/http" "time" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + "github.com/glebarez/sqlite" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/glebarez/sqlite" "gorm.io/gorm" ) @@ -17,18 +18,24 @@ import ( var f embed.FS type Model struct { - ID uint `gorm:"primaryKey;not null" json:"id"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + ID uint `gorm:"primaryKey;not null" json:"id"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } type ScheduleItem struct { Model - Name string `gorm:"not null" json:"name"` - Description *string `json:"description"` - Duration Duration `gorm:"not null;default:\"1h\"" json:"duration"` - Date time.Time `gorm:"not null" json:"date"` + Name string `gorm:"not null" json:"name"` + Description *string `json:"description"` + Duration Duration `gorm:"not null;default:\"1h\"" json:"duration"` + Date time.Time `gorm:"not null" json:"date"` + Tags []ScheduleTag `gorm:"many2many:schedule_item_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"tags"` +} + +type ScheduleTag struct { + Model + Name string `gorm:"not null;unique;index" json:"name"` } type NewScheduleItem struct { @@ -36,6 +43,11 @@ type NewScheduleItem struct { Description *string `json:"description" form:"description"` Duration *Duration `json:"duration" form:"duration"` Date *time.Time `json:"date" form:"date"` + Tags []string `json:"tags"` +} + +type NewScheduleTag struct { + Name *string `json:"name" form:"name"` } func getDateValues(c *fiber.Ctx) (time.Time, time.Time, error) { @@ -63,7 +75,7 @@ func getDateValues(c *fiber.Ctx) (time.Time, time.Time, error) { func dateRange(from time.Time, to time.Time) []time.Time { var dates []time.Time - for d := from; d.After(to) == false; d = d.AddDate(0, 0, 1) { + for d := from; !d.After(to); d = d.AddDate(0, 0, 1) { dates = append(dates, d) } @@ -76,13 +88,13 @@ func main() { log.Fatalf("failed to open database: %v", err) } - err = db.AutoMigrate(&ScheduleItem{}) + err = db.AutoMigrate(&ScheduleItem{}, &ScheduleTag{}) if err != nil { log.Fatalf("failed to migrate database: %v", err) } app := fiber.New() - + app.Use(cors.New()) app.Get("/api/items", func(c *fiber.Ctx) error { @@ -109,7 +121,7 @@ func main() { return err } - if input.Name == nil || len(*input.Name) < 0 { + if input.Name == nil || len(*input.Name) == 0 { return c.Status(fiber.StatusBadRequest).SendString("Name is required") } @@ -121,7 +133,7 @@ func main() { return c.Status(fiber.StatusBadRequest).SendString("Date is required") } - item, err := CreateScheduleItem(db, *input.Name, input.Description, *input.Duration, *input.Date) + item, err := CreateScheduleItem(db, *input.Name, input.Description, *input.Duration, *input.Date, input.Tags) if err != nil { log.Printf("failed to create schedule item: %v", err) @@ -144,7 +156,7 @@ func main() { log.Printf("failed to find schedule items: %v", err) return c.SendStatus(fiber.StatusInternalServerError) } - + schedule := map[string][]ScheduleItem{} for _, date := range dateRange(fromDate, toDate) { @@ -197,11 +209,83 @@ func main() { return c.SendStatus(http.StatusOK) }) - - app.Use(filesystem.New(filesystem.Config{ - Root: http.FS(f), - PathPrefix: "public", - })) + + app.Get("/api/tags", func(c *fiber.Ctx) error { + tags, err := GetScheduleTags(db) + + if err != nil { + log.Printf("failed to find schedule tags: %v", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + + return c.JSON(tags) + }) + + app.Post("/api/tags", func(c *fiber.Ctx) error { + input := new(NewScheduleTag) + + if err := c.BodyParser(input); err != nil { + return err + } + + if input.Name == nil || len(*input.Name) == 0 { + return c.Status(fiber.StatusBadRequest).SendString("Name is required") + } + + tag, err := CreateScheduleTag(db, *input.Name) + + if err != nil { + log.Printf("failed to create schedule tag: %v", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + + return c.JSON(tag) + }) + + app.Get("/api/tags/:id", func(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil { + return c.Status(fiber.StatusBadRequest).SendString("Invalid id") + } + + tag, err := GetScheduleTag(db, id) + + if err != nil { + log.Printf("failed to find schedule tag: %v", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + + if tag == nil { + return c.SendStatus(fiber.StatusNotFound) + } + + return c.JSON(*tag) + }) + + app.Delete("/api/tags/:id", func(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil { + return c.Status(fiber.StatusBadRequest).SendString("Invalid id") + } + + ok, err := DeleteScheduleTag(db, id) + + if err != nil { + log.Printf("failed to delete schedule item: %v", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + + if !ok { + return c.SendStatus(fiber.StatusNotFound) + } + + return c.SendStatus(http.StatusOK) + }) + + app.Use(filesystem.New(filesystem.Config{ + Root: http.FS(f), + PathPrefix: "public", + })) log.Fatal(app.Listen(":3000")) } diff --git a/public/index.html b/public/index.html deleted file mode 100644 index fbc52b8..0000000 --- a/public/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - Schedule - - - - -
- - -