Compare commits

...

2 Commits

Author SHA1 Message Date
brajkovic 67c72e8b4f Merge pull request 'v1.0.0' (#2) from feature/reorganizing-code into main
Reviewed-on: #2
2024-02-10 17:07:44 +00:00
Borna Rajković c6f365ab28 v1.0.0
Code reorganization + added meta tags and favicon
2024-02-10 17:49:00 +01:00
22 changed files with 566 additions and 415 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
holiday-api holiday-api
.env .env
**/.DS_Store **/.DS_Store
.env*

351
api/api.go Normal file
View File

@ -0,0 +1,351 @@
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"holiday-api/domain/holiday"
"log"
"net/http"
"strconv"
"time"
)
func NoMethod() gin.HandlerFunc {
return func(c *gin.Context) {
c.AbortWithError(http.StatusNotFound, nil)
}
}
func NoRoute() gin.HandlerFunc {
return func(c *gin.Context) {
c.AbortWithError(http.StatusNotFound, nil)
}
}
func GetIndex(holidayService holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) gin.HandlerFunc {
return func(c *gin.Context) {
search := holiday.Search{Country: "HR", Year: nil}
if err := c.ShouldBindQuery(&search); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
years, _ := yearService.Find()
if search.Year == nil {
c.HTML(http.StatusOK, "index.gohtml", gin.H{"Year": time.Now().Year(), "Years": years})
return
}
countries, _ := countryService.Find()
holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100})
c.HTML(http.StatusOK, "results.gohtml", gin.H{"Years": years, "Countries": countries, "Search": search, "Holidays": holiday.ToResponse(holidays).Holidays})
}
}
func GetDocumentation(countryService holiday.CountryService, yearService holiday.YearService) gin.HandlerFunc {
return func(c *gin.Context) {
countries, _ := countryService.Find()
years, _ := yearService.Find()
c.HTML(http.StatusOK, "documentation.gohtml", gin.H{"Year": time.Now().Year(), "Years": years, "Countries": countries})
}
}
func GetHolidays(service holiday.HolidayService) gin.HandlerFunc {
return func(c *gin.Context) {
paging := holiday.Paging{PageSize: 50}
if err := c.ShouldBindQuery(&paging); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
search := holiday.Search{}
if err := c.ShouldBindQuery(&search); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
holidays, err := service.Find(search, paging)
if err != nil {
render(c, http.StatusNotFound, holiday.ErrorResponse{Created: time.Now(), Message: "failed fetching holidays"}, nil)
} else {
render(c, http.StatusOK, holiday.ToResponse(holidays), search.Type)
}
}
}
func DeleteCountry(countryService holiday.CountryService) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
_, err := countryService.FindById(id)
if err != nil {
Abort(c, err, http.StatusNotFound, "couldn't find country")
return
}
if err := countryService.Delete(id); err != nil {
Abort(c, err, http.StatusInternalServerError, "couldn't delete country")
return
}
c.Redirect(http.StatusSeeOther, "/admin/countries")
}
}
func CreateOrUpdateCountry(countryService holiday.CountryService) gin.HandlerFunc {
return 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 {
Abort(c, err, http.StatusInternalServerError, "invalid request when creating or updating country")
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 {
Abort(c, err, http.StatusInternalServerError, "couldn't create or update country")
} else {
c.Redirect(http.StatusSeeOther, "/admin/countries")
}
}
}
func GetCountries(countryService holiday.CountryService) gin.HandlerFunc {
return func(c *gin.Context) {
countries, _ := countryService.Find()
c.HTML(http.StatusOK, "countries.gohtml", gin.H{"Countries": countries})
}
}
func DeleteHoliday(holidayService holiday.HolidayService) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
hol, err := holidayService.FindById(id)
if err != nil {
Abort(c, err, http.StatusNotFound, "couldn't find holiday to delete")
return
}
if err := holidayService.Delete(id); err != nil {
Abort(c, err, http.StatusInternalServerError, "couldn't delete holiday")
return
}
c.Redirect(http.StatusSeeOther, "/admin?country="+hol.Country+"&year="+strconv.FormatInt(int64(hol.Date.Year()), 10))
}
}
func CopyYear(holidayService holiday.HolidayService) gin.HandlerFunc {
return func(c *gin.Context) {
request := struct {
From int `form:"from"`
To int `form:"to"`
Country string `form:"country" binding:"len=2"`
}{}
if err := c.ShouldBind(&request); err != nil {
Abort(c, err, http.StatusBadRequest, "invalid request when copying holiday")
return
}
err := holidayService.Copy(request.Country, request.From, request.To)
if err != nil {
Abort(c, err, http.StatusInternalServerError, "couldn't copy holidays")
} else {
c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.To), 10))
}
}
}
func CreateOrUpdateHoliday(holidayService holiday.HolidayService) gin.HandlerFunc {
return 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 {
Abort(c, err, http.StatusBadRequest, "invalid request when creating holiday")
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 = holidayService.Update(hol)
} else {
hol, err = holidayService.Create(hol)
}
if err != nil {
Abort(c, err, 500, "couldn't create or update holiday")
} else {
c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.Date.Year()), 10))
}
}
}
func GetAdminHome(holidayService holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) gin.HandlerFunc {
return func(c *gin.Context) {
search := holiday.Search{Country: "HR", Year: new(int)}
*search.Year = time.Now().Year()
if err := c.ShouldBindQuery(&search); err != nil {
Abort(c, err, http.StatusBadRequest, "invalid search parameters")
return
}
holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100})
holidayResponse := holiday.ToResponse(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)
}
}
func AddHolidayDialog(countryService holiday.CountryService) gin.HandlerFunc {
return func(c *gin.Context) {
countries, _ := countryService.Find()
c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries})
}
}
func EditHolidayDialog(holidayService holiday.HolidayService, countryService holiday.CountryService) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Query("id"))
hol, err := holidayService.FindById(id)
if err != nil {
Abort(c, err, http.StatusNotFound, "couldn't find holiday to edit")
return
}
countries, _ := countryService.Find()
c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol})
}
}
func DeleteHolidayDialog(holidayService holiday.HolidayService) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Query("id"))
hol, err := holidayService.FindById(id)
if err != nil {
Abort(c, err, http.StatusNotFound, "couldn't find dialog")
return
}
c.HTML(http.StatusOK, "delete-holiday.gohtml", gin.H{"Holiday": hol})
}
}
func CopyYearDialog() gin.HandlerFunc {
return func(c *gin.Context) {
country := c.Query("country")
year, err := strconv.ParseInt(c.Query("year"), 10, 32)
if err != nil {
Abort(c, err, http.StatusNotFound, "couldn't parse year")
return
}
c.HTML(http.StatusOK, "copy-year.gohtml", gin.H{"Country": country, "Year": year})
}
}
func AddCountryDialog() gin.HandlerFunc {
return func(c *gin.Context) {
c.HTML(http.StatusOK, "add-country.gohtml", gin.H{})
}
}
func EditCountryDialog(countryService holiday.CountryService) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Query("id"))
country, err := countryService.FindById(id)
if err != nil {
Abort(c, err, http.StatusNotFound, "couldn't find country")
return
}
c.HTML(http.StatusOK, "edit-country.gohtml", gin.H{"Country": country})
}
}
func DeleteCountryDialog(countryService holiday.CountryService) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Query("id"))
country, err := countryService.FindById(id)
if err != nil {
Abort(c, err, http.StatusNotFound, "couldn't find dialog")
return
}
c.HTML(http.StatusOK, "delete-country.gohtml", gin.H{"Country": country})
}
}
type CSV interface {
CSV() []byte
}
func render(c *gin.Context, status int, response any, contentType *string) {
value := c.GetHeader("accept")
if contentType != nil {
switch *contentType {
case "xml":
value = "text/xml"
case "json":
value = "application/json"
case "csv":
value = "text/csv"
}
}
switch value {
case "text/csv":
if csvResponse, ok := response.(CSV); ok {
c.Data(200, value+"; charset=utf-8", csvResponse.CSV())
} else {
c.Header("content-type", "application/json; charset=utf-8")
c.JSON(status, response)
}
case "application/xml":
fallthrough
case "text/xml":
c.Header("content-type", value+"; charset=utf-8; header=present;")
c.XML(status, response)
case "application/json":
fallthrough
default:
c.Header("content-type", "application/json; charset=utf-8")
c.JSON(status, response)
}
}
func Abort(c *gin.Context, err error, statusCode int, message string) {
log.Output(1, fmt.Sprintf("error | %s | err: %v", message, errorMessage(err)))
c.AbortWithError(statusCode, err)
}
func errorMessage(err error) string {
if err != nil {
return err.Error()
}
return "-"
}

39
api/middleware.go Normal file
View File

@ -0,0 +1,39 @@
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"os"
"strings"
)
func Auth() gin.HandlerFunc {
if hasProfile("basic-auth") {
return gin.BasicAuth(loadAuth())
}
panic("keycloak support not implemented")
}
func hasProfile(value string) bool {
profileOptions := strings.Split(os.Getenv("PROFILE"), ",")
for _, option := range profileOptions {
if option == value {
return true
}
}
return false
}
func loadAuth() map[string]string {
credentials := envMustExist("AUTH_KEY")
values := strings.Split(credentials, ":")
return map[string]string{values[0]: values[1]}
}
func envMustExist(env string) string {
if value, exists := os.LookupEnv(env); !exists {
panic(fmt.Sprintf("env variable '%s' not defined", env))
} else {
return value
}
}

39
api/routes.go Normal file
View File

@ -0,0 +1,39 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
"holiday-api/domain/holiday"
)
func RegisterRoutes(server *gin.Engine, db *sqlx.DB) {
holidayService := holiday.HolidayService{DB: db}
countryService := holiday.CountryService{DB: db}
yearService := holiday.YearService{DB: db}
server.GET("/", GetIndex(holidayService, countryService, yearService))
server.GET("/documentation", GetDocumentation(countryService, yearService))
server.GET("/api/v1/holidays", GetHolidays(holidayService))
auth := Auth()
adminGroup := server.Group("/admin", auth)
adminGroup.GET("/", GetAdminHome(holidayService, countryService, yearService))
adminGroup.POST("/holidays", CreateOrUpdateHoliday(holidayService))
adminGroup.POST("/holidays/copy", CopyYear(holidayService))
adminGroup.POST("/holidays/:id/delete", DeleteHoliday(holidayService))
adminGroup.GET("countries", GetCountries(countryService))
adminGroup.POST("countries", CreateOrUpdateCountry(countryService))
adminGroup.POST("countries/:id/delete", DeleteCountry(countryService))
dialogGroup := server.Group("/admin/dialogs", auth)
dialogGroup.GET("/add-country", AddCountryDialog())
dialogGroup.GET("/edit-country", EditCountryDialog(countryService))
dialogGroup.GET("/delete-country", DeleteCountryDialog(countryService))
dialogGroup.GET("/add-holiday", AddHolidayDialog(countryService))
dialogGroup.GET("/edit-holiday", EditHolidayDialog(holidayService, countryService))
dialogGroup.GET("/delete-holiday", DeleteHolidayDialog(holidayService))
dialogGroup.GET("/copy-year", CopyYearDialog())
}

22
api/server.go Normal file
View File

@ -0,0 +1,22 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
)
func SetupServer(db *sqlx.DB) *gin.Engine {
server := createServer()
LoadTemplates(server)
RegisterRoutes(server, db)
return server
}
func createServer() *gin.Engine {
server := gin.New()
server.NoRoute(NoRoute())
server.NoMethod(NoMethod())
server.Use(gin.Recovery())
server.Static("assets", "assets")
return server
}

43
api/templates.go Normal file
View File

@ -0,0 +1,43 @@
package api
import (
"github.com/gin-gonic/gin"
"html/template"
)
func LoadTemplates(server *gin.Engine) {
server.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
},
})
server.LoadHTMLFiles(
"templates/index.gohtml",
"templates/results.gohtml",
"templates/documentation.gohtml",
"templates/admin_dashboard.gohtml",
"templates/countries.gohtml",
"templates/dialogs/copy-year.gohtml",
"templates/dialogs/add-holiday.gohtml",
"templates/dialogs/edit-holiday.gohtml",
"templates/dialogs/delete-holiday.gohtml",
"templates/dialogs/add-country.gohtml",
"templates/dialogs/edit-country.gohtml",
"templates/dialogs/delete-country.gohtml",
)
}

BIN
assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

16
db.go
View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"holiday-api/db"
"os" "os"
) )
@ -22,18 +23,5 @@ func connectToDb() (*sqlx.DB, error) {
password := envMustExist("PSQL_PASSWORD") password := envMustExist("PSQL_PASSWORD")
dbname := envMustExist("PSQL_DB") dbname := envMustExist("PSQL_DB")
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", return db.ConnectToDbNamed(host, port, user, password, dbname)
host, port, user, password, dbname)
db, err := sqlx.Open("postgres", psqlInfo)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return nil, err
}
return db, nil
} }

30
db/database.go Normal file
View File

@ -0,0 +1,30 @@
package db
import (
"embed"
"fmt"
"github.com/jmoiron/sqlx"
)
//go:embed dev/*.sql
var DevMigrations embed.FS
//go:embed prod/*.sql
var ProdMigrations embed.FS
func ConnectToDbNamed(host string, port string, user string, password string, dbname string) (*sqlx.DB, error) {
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
db, err := sqlx.Open("postgres", psqlInfo)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return nil, err
}
return db, nil
}

View File

@ -1,41 +1,15 @@
package main package holiday
import ( import (
"bytes" "bytes"
"encoding/csv" "encoding/csv"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"holiday-api/holiday"
"net/http"
"strconv" "strconv"
"time" "time"
) )
func getHolidays(service holiday.HolidayService) gin.HandlerFunc {
return func(c *gin.Context) {
paging := holiday.Paging{PageSize: 50}
if err := c.ShouldBindQuery(&paging); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
search := holiday.Search{}
if err := c.ShouldBindQuery(&search); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
holidays, err := service.Find(search, paging)
if err != nil {
render(c, http.StatusNotFound, ErrorResponse{Created: time.Now(), Message: "failed fetching holidays"}, nil)
} else {
render(c, http.StatusOK, mapHolidays(holidays), search.Type)
}
}
}
type DateResponse struct{ time.Time } type DateResponse struct{ time.Time }
func (d DateResponse) MarshalJSON() ([]byte, error) { func (d DateResponse) MarshalJSON() ([]byte, error) {
@ -79,27 +53,7 @@ type HolidayItemResponse struct {
IsReligiousHoliday bool `json:"isReligiousHoliday" xml:"isReligious,attr"` IsReligiousHoliday bool `json:"isReligiousHoliday" xml:"isReligious,attr"`
} }
type HolidaySingleResponse struct { func ToResponse(holidays []Holiday) HolidayResponse {
Id *uuid.UUID
Country string
Date DateResponse
Name string
Description string
IsStateHoliday bool
IsReligiousHoliday bool
}
type HolidaySingleRequest struct {
Id *uuid.UUID
Country string
Date DateResponse
Name string
Description string
IsStateHoliday bool
IsReligiousHoliday bool
}
func mapHolidays(holidays []holiday.Holiday) HolidayResponse {
var response = make([]HolidayItemResponse, 0, len(holidays)) var response = make([]HolidayItemResponse, 0, len(holidays))
for _, h := range holidays { for _, h := range holidays {
response = append(response, HolidayItemResponse{ response = append(response, HolidayItemResponse{

312
main.go
View File

@ -1,331 +1,51 @@
package main package main
import ( import (
"embed"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/joho/godotenv" "github.com/joho/godotenv"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"holiday-api/holiday" "holiday-api/api"
"holiday-api/db"
"holiday-api/migration" "holiday-api/migration"
"html/template" "io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "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() { func init() {
godotenv.Load() godotenv.Load()
if strings.Contains(os.Getenv("PROFILE"), "dev") {
isDev = true
}
log.SetPrefix("") log.SetPrefix("")
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
} }
func main() { func main() {
client, err := connectToDb() db, err := connectToDb()
if err != nil { if err != nil {
log.Fatalf("couldn't connect to db: %v", err) log.Fatalf("couldn't connect to db: %v", err)
} }
migrationFolder := prodMigrations if err := migration.InitializeMigrations(db, migrationFolder()); err != nil {
if isDev {
migrationFolder = devMigrations
}
if err := migration.InitializeMigrations(client, migrationFolder); err != nil {
log.Fatalf("couldn't execute migrations: %v", err) log.Fatalf("couldn't execute migrations: %v", err)
} }
g := gin.Default() server := api.SetupServer(db)
g.Static("assets", "assets") log.Fatal(http.ListenAndServe(":5281", server))
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
}
years, _ := yearService.Find()
if search.Year == nil {
c.HTML(http.StatusOK, "index.gohtml", gin.H{"Year": time.Now().Year(), "Years": years})
return
}
countries, _ := countryService.Find()
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{"Year": time.Now().Year(), "Years": years, "Countries": countries})
})
log.Fatal(http.ListenAndServe(":5281", g))
} }
func loadTemplates(g *gin.Engine) { func hasProfile(value string) bool {
g.SetFuncMap(template.FuncMap{ profileOptions := strings.Split(os.Getenv("PROFILE"), ",")
"boolcmp": func(value *bool, expected string) bool { for _, option := range profileOptions {
if value == nil { if option == value {
return expected == "nil" return true
} 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 return false
},
"importSvg": IncludeHTML,
})
g.LoadHTMLFiles(
"templates/index.gohtml",
"templates/results.gohtml",
"templates/documentation.gohtml",
"templates/admin_dashboard.gohtml",
"templates/countries.gohtml",
"templates/dialogs/copy-year.gohtml",
"templates/dialogs/add-holiday.gohtml",
"templates/dialogs/edit-holiday.gohtml",
"templates/dialogs/delete-holiday.gohtml",
"templates/dialogs/add-country.gohtml",
"templates/dialogs/edit-country.gohtml",
"templates/dialogs/delete-country.gohtml",
)
} }
func IncludeHTML(path string) template.HTML { func migrationFolder() fs.FS {
b, err := os.ReadFile(path) if hasProfile("dev") {
if err != nil { return db.DevMigrations
log.Println("includeHTML - error reading file: %v", err)
return ""
} }
return template.HTML(string(b)) return db.ProdMigrations
}
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/copy", func(c *gin.Context) {
request := struct {
From int `form:"from"`
To int `form:"to"`
Country string `form:"country" binding:"len=2"`
}{}
if err := c.ShouldBind(&request); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
err := service.Copy(request.Country, request.From, request.To)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
} else {
c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.To), 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/copy-year", func(c *gin.Context) {
country := c.Query("country")
year, err := strconv.ParseInt(c.Query("year"), 10, 32)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
c.HTML(http.StatusOK, "copy-year.gohtml", gin.H{"Country": country, "Year": year})
})
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]}
} }

View File

@ -1,7 +1,7 @@
# scripts for building app # scripts for building app
# requires go 1.19+ and git installed # requires go 1.19+ and git installed
VERSION := 0.5.0 VERSION := 1.0.0
serve: serve:
go run ./... go run ./...

View File

@ -1,42 +0,0 @@
package main
import (
"github.com/gin-gonic/gin"
)
type CSV interface {
CSV() []byte
}
func render(c *gin.Context, status int, response any, contentType *string) {
value := c.GetHeader("accept")
if contentType != nil {
switch *contentType {
case "xml":
value = "text/xml"
case "json":
value = "application/json"
case "csv":
value = "text/csv"
}
}
switch value {
case "text/csv":
if csvResponse, ok := response.(CSV); ok {
c.Data(200, value+"; charset=utf-8", csvResponse.CSV())
} else {
c.Header("content-type", "application/json; charset=utf-8")
c.JSON(status, response)
}
case "application/xml":
fallthrough
case "text/xml":
c.Header("content-type", value+"; charset=utf-8; header=present;")
c.XML(status, response)
case "application/json":
fallthrough
default:
c.Header("content-type", "application/json; charset=utf-8")
c.JSON(status, response)
}
}

View File

@ -5,7 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="favicon.ico" type="image/x-icon"> <link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/assets/global.css"> <link rel="stylesheet" href="/assets/global.css">
<script src="/assets/global.js"></script> <script src="/assets/global.js"></script>
</head> </head>

View File

@ -5,7 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="favicon.ico" type="image/x-icon"> <link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/assets/global.css"> <link rel="stylesheet" href="/assets/global.css">
<script src="/assets/global.js"></script> <script src="/assets/global.js"></script>
</head> </head>

View File

@ -1,11 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="hr">
<head> <head>
<title>Holiday-api | Dokumentacija</title> <title>Holiday-api | Api dokumentacija</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="favicon.ico" type="image/x-icon"> <meta name="description" content="Preuzmi praznike za neki vremenski raspon programski u JSON, XML ili CSV formatu"/>
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/assets/global.css"> <link rel="stylesheet" href="/assets/global.css">
<script src="/assets/global.js"></script> <script src="/assets/global.js"></script>
<script src="/assets/documentation.js"></script> <script src="/assets/documentation.js"></script>

View File

@ -1,11 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="hr">
<head> <head>
<title>Holiday-api</title> <title>Holiday-api | Pronađi praznike</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="favicon.ico" type="image/x-icon"> <meta name="description" content="Pronađi listu praznike za neki vremenski raspon, ili kao web stranicu ili programski koristeći naš api u JSON, XML ili CSV formatu"/>
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/assets/global.css"> <link rel="stylesheet" href="/assets/global.css">
<script src="/assets/global.js"></script> <script src="/assets/global.js"></script>
</head> </head>

View File

@ -1,11 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="hr">
<head> <head>
<title>Holiday-api | {{deferint $.Search.Year}}</title> <title>Holiday-api | Praznici za {{deferint $.Search.Year}}</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="favicon.ico" type="image/x-icon"> <meta name="description" content="Lista praznika za godinu {{deferint $.Search.Year}}. Praznici ovo ili neku drugu godinu pronađi ovdje. "/>
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/assets/global.css"> <link rel="stylesheet" href="/assets/global.css">
<script src="/assets/global.js"></script> <script src="/assets/global.js"></script>