package main import ( "embed" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/joho/godotenv" _ "github.com/lib/pq" "holiday-api/holiday" "holiday-api/migration" "html/template" "log" "net/http" "os" "strconv" "strings" "time" ) //go:embed db/dev/*.sql var devMigrations embed.FS //go:embed db/prod/*.sql var prodMigrations embed.FS var isDev = false func init() { godotenv.Load() if strings.Contains(os.Getenv("PROFILE"), "dev") { isDev = true } log.SetPrefix("") log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) } func main() { client, err := connectToDb() if err != nil { log.Fatalf("couldn't connect to db: %v", err) } migrationFolder := prodMigrations if isDev { migrationFolder = devMigrations } if err := migration.InitializeMigrations(client, migrationFolder); err != nil { log.Fatalf("couldn't execute migrations: %v", err) } g := gin.Default() g.Static("assets", "assets") loadTemplates(g) holidayService := holiday.HolidayService{DB: client} countryService := holiday.CountryService{DB: client} yearService := holiday.YearService{DB: client} g.GET("/api/v1/holidays", getHolidays(holidayService)) setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService) g.GET("/", func(c *gin.Context) { search := holiday.Search{Country: "HR", Year: nil} if err := c.ShouldBindQuery(&search); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } countries, _ := countryService.Find() years, _ := yearService.Find() if search.Year == nil { c.HTML(http.StatusOK, "index.gohtml", gin.H{"Years": years, "Countries": countries, "Search": search}) return } holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100}) c.HTML(http.StatusOK, "results.gohtml", gin.H{"Years": years, "Countries": countries, "Search": search, "Holidays": mapHolidays(holidays).Holidays}) }) g.GET("/documentation", func(c *gin.Context) { countries, _ := countryService.Find() years, _ := yearService.Find() c.HTML(http.StatusOK, "documentation.gohtml", gin.H{"Years": years, "Countries": countries}) }) g.GET("/search", func(c *gin.Context) { request := holiday.Search{} if err := c.ShouldBindQuery(&request); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } search := holiday.Search{Country: request.Country, Date: request.Date} holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100}) countries, _ := countryService.Find() c.HTML(http.StatusOK, "search.gohtml", gin.H{"Countries": countries, "Search": search, "Holidays": mapHolidays(holidays).Holidays}) }) g.GET("/dialogs/check-is-a-holiday", func(c *gin.Context) { countries, _ := countryService.Find() c.HTML(http.StatusOK, "check-is-a-holiday.gohtml", gin.H{"Countries": countries}) }) log.Fatal(http.ListenAndServe(":5281", g)) } func loadTemplates(g *gin.Engine) { g.SetFuncMap(template.FuncMap{ "boolcmp": func(value *bool, expected string) bool { if value == nil { return expected == "nil" } else { return (*value && expected == "true") || (!(*value) && expected == "false") } }, "deferint": func(value *int) int { return *value }, "intpeq": func(selected *int, value int) bool { if selected != nil { return *selected == value } return false }, "importSvg": IncludeHTML, }) g.LoadHTMLFiles( "templates/index.gohtml", "templates/results.gohtml", "templates/search.gohtml", "templates/documentation.gohtml", "templates/admin_dashboard.gohtml", "templates/countries.gohtml", "templates/dialogs/add-holiday.gohtml", "templates/dialogs/edit-holiday.gohtml", "templates/dialogs/delete-holiday.gohtml", "templates/dialogs/check-is-a-holiday.gohtml", "templates/dialogs/add-country.gohtml", "templates/dialogs/edit-country.gohtml", "templates/dialogs/delete-country.gohtml", ) } func IncludeHTML(path string) template.HTML { b, err := os.ReadFile(path) if err != nil { log.Println("includeHTML - error reading file: %v", err) return "" } return template.HTML(string(b)) } func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) { adminDashboard.Use(gin.BasicAuth(loadAuth())) adminDashboard.GET("/", func(c *gin.Context) { search := holiday.Search{Country: "HR", Year: new(int)} *search.Year = time.Now().Year() if err := c.ShouldBindQuery(&search); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } holidays, _ := service.Find(search, holiday.Paging{PageSize: 100}) holidayResponse := mapHolidays(holidays) countries, _ := countryService.Find() years, _ := yearService.Find() response := map[string]any{} response["Holidays"] = holidayResponse response["Search"] = search response["Countries"] = countries response["Years"] = years c.HTML(http.StatusOK, "admin_dashboard.gohtml", response) }) adminDashboard.POST("/holidays", func(c *gin.Context) { request := struct { Id *string `form:"id"` Name string `form:"name" binding:"required,min=1"` Description string `form:"description"` IsStateHoliday bool `form:"state_holiday"` IsReligiousHoliday bool `form:"religious_holiday"` Country string `form:"country" binding:"len=2"` Date time.Time `form:"date" time_format:"2006-01-02"` }{} if err := c.ShouldBind(&request); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } hol := holiday.Holiday{ Country: request.Country, Date: request.Date, Name: request.Name, Description: request.Description, IsStateHoliday: request.IsStateHoliday, IsReligiousHoliday: request.IsReligiousHoliday, } var err error if request.Id != nil { hol.Id = uuid.MustParse(*request.Id) hol, err = service.Update(hol) } else { hol, err = service.Create(hol) } if err != nil { c.AbortWithError(http.StatusInternalServerError, err) } else { c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.Date.Year()), 10)) } }) adminDashboard.POST("/holidays/:id/delete", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) hol, err := service.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if err := service.Delete(id); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.Redirect(http.StatusSeeOther, "/admin?country="+hol.Country+"&year="+strconv.FormatInt(int64(hol.Date.Year()), 10)) }) adminDashboard.GET("/countries", func(c *gin.Context) { countries, _ := countryService.Find() c.HTML(http.StatusOK, "countries.gohtml", gin.H{"Countries": countries}) }) adminDashboard.POST("/countries", func(c *gin.Context) { request := struct { Id *string `form:"id"` IsoName string `form:"iso_name" binding:"required,min=2,max=2"` Name string `form:"name" binding:"required,min=1,max=45"` }{} if err := c.ShouldBind(&request); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } country := holiday.Country{ IsoName: request.IsoName, Name: request.Name, } var err error if request.Id != nil { country.Id = uuid.MustParse(*request.Id) country, err = countryService.Update(country) } else { country, err = countryService.Create(country) } if err != nil { c.AbortWithError(http.StatusInternalServerError, err) } else { c.Redirect(http.StatusSeeOther, "/admin/countries") } }) adminDashboard.POST("/countries/:id/delete", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) _, err := countryService.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if err := countryService.Delete(id); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.Redirect(http.StatusSeeOther, "/admin/countries") }) adminDashboard.GET("/dialogs/add-country", func(c *gin.Context) { c.HTML(http.StatusOK, "add-country.gohtml", gin.H{}) }) adminDashboard.GET("/dialogs/add-holiday", func(c *gin.Context) { countries, _ := countryService.Find() c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries}) }) adminDashboard.GET("/dialogs/edit-holiday", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) hol, err := service.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } countries, _ := countryService.Find() c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol}) }) adminDashboard.GET("/dialogs/edit-country", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) country, err := countryService.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } c.HTML(http.StatusOK, "edit-country.gohtml", gin.H{"Country": country}) }) adminDashboard.GET("/dialogs/delete-holiday", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) hol, err := service.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } c.HTML(http.StatusOK, "delete-holiday.gohtml", gin.H{"Holiday": hol}) }) adminDashboard.GET("/dialogs/delete-country", func(c *gin.Context) { id := uuid.MustParse(c.Query("id")) country, err := countryService.FindById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } c.HTML(http.StatusOK, "delete-country.gohtml", gin.H{"Country": country}) }) } func loadAuth() map[string]string { credentials := envMustExist("AUTH_KEY") values := strings.Split(credentials, ":") return map[string]string{values[0]: values[1]} }