package main import ( "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" "gorm.io/gorm" ) //go:embed public/* 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:"-"` } 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"` 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 { Name *string `json:"name" form:"name"` 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) { var err error from := time.Now() to := time.Now().Add(time.Hour * 24 * time.Duration(c.QueryInt("days", 7))) if rawFromDate := c.Query("from"); rawFromDate != "" { from, err = time.Parse("2006-01-02", rawFromDate) if err != nil { return from, to, err } } if rawToDate := c.Query("to"); rawToDate != "" { to, err = time.Parse("2006-01-02", rawToDate) if err != nil { return from, to, err } } return from, to, nil } func dateRange(from time.Time, to time.Time) []time.Time { var dates []time.Time for d := from; !d.After(to); d = d.AddDate(0, 0, 1) { dates = append(dates, d) } return dates[:len(dates)-1] } func main() { db, err := gorm.Open(sqlite.Open("schedule.db"), &gorm.Config{}) if err != nil { log.Fatalf("failed to open database: %v", err) } 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 { fromDate, toDate, err := getDateValues(c) if err != nil { return c.Status(fiber.StatusBadRequest).SendString("Invalid date query values") } items, err := GetScheduleItems(db, fromDate, toDate) if err != nil { log.Printf("failed to find schedule items: %v", err) return c.SendStatus(fiber.StatusInternalServerError) } return c.JSON(items) }) app.Post("/api/items", func(c *fiber.Ctx) error { input := new(NewScheduleItem) 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") } if input.Duration == nil { input.Duration = &Duration{Duration: time.Hour} } if input.Date == nil { return c.Status(fiber.StatusBadRequest).SendString("Date is required") } 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) return c.SendStatus(fiber.StatusInternalServerError) } return c.JSON(item) }) app.Get("/api/schedule", func(c *fiber.Ctx) error { fromDate, toDate, err := getDateValues(c) if err != nil { return c.Status(fiber.StatusBadRequest).SendString("Invalid date query values") } items, err := GetScheduleItems(db, fromDate, toDate) if err != nil { log.Printf("failed to find schedule items: %v", err) return c.SendStatus(fiber.StatusInternalServerError) } schedule := map[string][]ScheduleItem{} for _, date := range dateRange(fromDate, toDate) { schedule[date.Format(time.DateOnly)] = []ScheduleItem{} } for _, item := range items { schedule[item.Date.Format(time.DateOnly)] = append(schedule[item.Date.Format(time.DateOnly)], item) } return c.JSON(schedule) }) app.Get("/api/items/:id", func(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil { return c.Status(fiber.StatusBadRequest).SendString("Invalid id") } item, err := GetScheduleItem(db, id) if err != nil { log.Printf("failed to find schedule item: %v", err) return c.SendStatus(fiber.StatusInternalServerError) } if item == nil { return c.SendStatus(fiber.StatusNotFound) } return c.JSON(*item) }) app.Delete("/api/items/:id", func(c *fiber.Ctx) error { id, err := c.ParamsInt("id") if err != nil { return c.Status(fiber.StatusBadRequest).SendString("Invalid id") } ok, err := DeleteScheduleItem(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.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")) }