diff --git a/.gitignore b/.gitignore index 0c6d955..4cbb524 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/** holiday-api -.env \ No newline at end of file +.env +**/.DS_Store diff --git a/assets/background.jpg b/assets/background.jpg new file mode 100755 index 0000000..2b28ea9 Binary files /dev/null and b/assets/background.jpg differ diff --git a/assets/fonts/Quicksand-Bold.ttf b/assets/fonts/Quicksand-Bold.ttf new file mode 100755 index 0000000..725ee41 Binary files /dev/null and b/assets/fonts/Quicksand-Bold.ttf differ diff --git a/assets/fonts/Quicksand-Light.ttf b/assets/fonts/Quicksand-Light.ttf new file mode 100755 index 0000000..c7859e1 Binary files /dev/null and b/assets/fonts/Quicksand-Light.ttf differ diff --git a/assets/fonts/Quicksand-Medium.ttf b/assets/fonts/Quicksand-Medium.ttf new file mode 100755 index 0000000..d4b02c5 Binary files /dev/null and b/assets/fonts/Quicksand-Medium.ttf differ diff --git a/assets/fonts/Quicksand-Regular.ttf b/assets/fonts/Quicksand-Regular.ttf new file mode 100755 index 0000000..980edda Binary files /dev/null and b/assets/fonts/Quicksand-Regular.ttf differ diff --git a/assets/fonts/Quicksand-SemiBold.ttf b/assets/fonts/Quicksand-SemiBold.ttf new file mode 100755 index 0000000..77f20d6 Binary files /dev/null and b/assets/fonts/Quicksand-SemiBold.ttf differ diff --git a/assets/global.css b/assets/global.css new file mode 100644 index 0000000..778b1e4 --- /dev/null +++ b/assets/global.css @@ -0,0 +1,203 @@ +body { + border: 0; + margin: 0; + padding: 0; + font-size: 20px; +} + +main { + background: #f3ebe6; + width: 640px; + max-width: 100%; + position: relative; +} + +.dialog { + box-sizing: border-box; + border: 1px solid #e0e0e0; + background: white; + margin: auto; + padding: 16px 32px; +} + +.background-image { + min-width: 100vw; + min-height: 100vh; + + max-width: 100vw; + max-height: 100vh; + + background: url("/assets/background.jpg"); + background-position: bottom; + background-size: cover; +} + +@font-face { + font-family: Quicksand; + src: url(fonts/Quicksand-Light.ttf); + font-weight: 100 200; +} +@font-face { + font-family: Quicksand; + src: url(fonts/Quicksand-Medium.ttf); + font-weight: 300 300; +} +@font-face { + font-family: Quicksand; + src: url(fonts/Quicksand-Regular.ttf); + font-weight: 400 500; +} +@font-face { + font-family: Quicksand; + src: url(fonts/Quicksand-Bold.ttf); + font-weight: 800 900; +} +@font-face { + font-family: Quicksand; + src: url(fonts/Quicksand-SemiBold.ttf); + font-weight: 600 700; +} + +* { + font-family: Quicksand, Arial, sans-serif; +} + +h1 { + font-size: 2rem; +} +h1 a { + all: unset; +} + + +.form-field { + width: 100%; + margin-bottom: 16px; +} +.form-field label { + display: block; + color: #999; +} +.form-field input { + width: 300px; + border: none; + border-bottom: 1px solid #bbb; + + font-size: 20px; + outline: none; +} + +.form-field input:focus { + color: cadetblue; + border-bottom: 1px solid cadetblue; +} + +.form-field input:not(focus) { + color: #333; +} + +.form-field label { + display: block; + color: #999; +} +.form-field select { + all: unset; + width: 100%; + border: none; + border-bottom: 1px solid #bbb; + + font-size: 20px; + outline: none; +} + +.form-field select:focus { + color: cadetblue; + border-bottom: 1px solid cadetblue; +} + +.form-field select:not(focus) { + color: #333; +} + +.form-field:focus-within label { + color: cadetblue; +} + + +.button.secondary, button.primary { + padding: 12px 16px; + background-color: cadetblue; + font-family: Quicksand, Arial, sans-serif; + border: none; + color: white; + font-size: 1.2rem; + margin-top: 16px; +} + +.button.secondary, button.primary:hover { + filter: contrast(140%); +} + +.button.secondary, button.secondary { + padding: 12px 16px; + background-color: white; + font-family: Quicksand, Arial, sans-serif; + border: 1px solid cadetblue; + color: cadetblue; + font-size: 1.2rem; + margin-top: 16px; +} + +.button.secondary:hover, button.secondary:hover { + filter: contrast(140%); +} + +/*index.html*/ + +.search-dialog { + position: absolute; + top: 0; + left: 0; + width: 100%; + padding-bottom: 40px; +} + +@media screen and (min-width: 480px) { + .search-dialog { + position: absolute; + top: 50%; + left: 10vw; + transform: translateY(-50%); + width: 380px; + padding-bottom: 40px; + } +} + +/*results.html*/ +.results-dialog { + position: relative; + width: 100%; + max-width: 100%; + padding-bottom: 40px; +} + +.results-dialog table { + width: 100%; +} + +.results-dialog table th, .results-dialog table td { + text-align: left; +} + +@media screen and (min-width: 480px) { + .results-dialog { + position: relative; + margin: auto; + width: 600px; + padding-bottom: 40px; + } +} + +.results td { + padding: 0.4em 0; +} \ No newline at end of file diff --git a/assets/global.js b/assets/global.js index 660fb8a..67d07a4 100644 --- a/assets/global.js +++ b/assets/global.js @@ -1,6 +1,19 @@ const dialogContainerId = "#dialog-container" window.addEventListener('load', () => { + document.querySelectorAll(".dropdown").forEach(el => { + const action = el.querySelector(".dropdown-action"); + const content = el.querySelector(".dropdown-content"); + + console.log(action, content, el.offsetWidth, content.offsetWidth); + + action.addEventListener('click', () => { + content.classList.toggle('selected'); + console.log(content.offsetWidth - action.offsetWidth); + content.style.marginLeft = `-${(content.offsetWidth - action.offsetWidth)}px`; + }) + }) + // configure radio button groups document.querySelectorAll("section.radio-group").forEach(el => { const submittedInput = el.querySelector("input[type=hidden]"); @@ -23,7 +36,13 @@ window.addEventListener('load', () => { let response = await fetch(btn.dataset.url); if(response.ok) { response = await response.text(); - const container = document.querySelector(dialogContainerId); + let container = document.querySelector(dialogContainerId); + if(container == null) { + const node = document.createElement('div'); + node.id = "dialog-container"; + document.querySelector("body").appendChild(node); + container = document.querySelector(dialogContainerId); + } container.innerHTML = response; const dialogReference = document.querySelector(selector); dialogReference?.showModal(); diff --git a/assets/images/circle-add.svg b/assets/icons/circle-add.svg similarity index 98% rename from assets/images/circle-add.svg rename to assets/icons/circle-add.svg index 6e3dfc8..26737a5 100644 --- a/assets/images/circle-add.svg +++ b/assets/icons/circle-add.svg @@ -1,5 +1,5 @@ - - - - + + + + \ No newline at end of file diff --git a/assets/images/close-x.svg b/assets/icons/close-x.svg similarity index 98% rename from assets/images/close-x.svg rename to assets/icons/close-x.svg index 1d0a4cd..fc8cc4d 100644 --- a/assets/images/close-x.svg +++ b/assets/icons/close-x.svg @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/assets/images/done-v.svg b/assets/icons/done-v.svg similarity index 98% rename from assets/images/done-v.svg rename to assets/icons/done-v.svg index 13be0f9..4a60a59 100644 --- a/assets/images/done-v.svg +++ b/assets/icons/done-v.svg @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/assets/images/edit.svg b/assets/icons/edit.svg similarity index 99% rename from assets/images/edit.svg rename to assets/icons/edit.svg index 597a307..06289f8 100644 --- a/assets/images/edit.svg +++ b/assets/icons/edit.svg @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/assets/images/search.svg b/assets/icons/search.svg similarity index 98% rename from assets/images/search.svg rename to assets/icons/search.svg index bd4c0b4..b857f91 100644 --- a/assets/images/search.svg +++ b/assets/icons/search.svg @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/assets/images/trash-delete.svg b/assets/icons/trash-delete.svg similarity index 99% rename from assets/images/trash-delete.svg rename to assets/icons/trash-delete.svg index e53609b..ce5b24f 100644 --- a/assets/images/trash-delete.svg +++ b/assets/icons/trash-delete.svg @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/assets/style.css b/assets/style.css index f5f081b..567de95 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,5 +1,11 @@ /* cleanup */ +:root { + --primary: #9222dd; + --primary-darker: #6400a2; + --primary-contrast: #eee; +} + * { box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; @@ -10,22 +16,26 @@ header { padding: 1rem 2rem; width: 100%; - background: #666; + background: var(--primary); + color: var(--primary-contrast); } header h1 { font-size: 1.25em; } header a { - color: #fff; + color: var(--primary-contrast); text-decoration: none; } button { all: unset; padding: 0.25em 0.5em; - background: #dddddd; - border: solid 1px #cccccc; + color: var(--primary-contrast); + background: var(--primary); + border: solid 1px var(--primary-darker); border-radius: 0.25em; font-size: 0.875em; + + --text: var(--primary-contrast); } select { all: unset; @@ -63,6 +73,10 @@ nav a.selected, nav button.selected { color: white; } +table { + border-radius: 0.5em; +} + table:not(.clean) { width: 100%; } @@ -71,7 +85,8 @@ table th, table td { text-align: left; } table { - border-collapse: collapse; + border-spacing: 0; + /*border-collapse: collapse;*/ } table thead * { background: #666; @@ -184,9 +199,9 @@ section.radio-group div input:checked+label { color: white; } -img.icon { - height: 1.5em; - width: 1.5em; +img.icon, button.icon svg { + height: 1.2em; + width: 1.2em; } button.icon, a.icon { display: flex; @@ -194,6 +209,11 @@ button.icon, a.icon { gap: 0.3em; } +button.icon svg, button.icon svg * { + fill: var(--text, var(--primary-contrast)); + color: var(--text, var(--primary-contrast)); + stroke: var(--text, var(--primary-contrast)); +} main { display: flex; diff --git a/handlers.go b/handlers.go index 947a682..5e3af2f 100644 --- a/handlers.go +++ b/handlers.go @@ -1,12 +1,15 @@ package main import ( + "bytes" + "encoding/csv" "encoding/xml" "fmt" "github.com/gin-gonic/gin" "github.com/google/uuid" "holiday-api/holiday" "net/http" + "strconv" "time" ) @@ -26,9 +29,9 @@ func getHolidays(service holiday.HolidayService) gin.HandlerFunc { holidays, err := service.Find(search, paging) if err != nil { - render(c, http.StatusNotFound, ErrorResponse{Created: time.Now(), Message: "failed fetching holidays"}) + render(c, http.StatusNotFound, ErrorResponse{Created: time.Now(), Message: "failed fetching holidays"}, nil) } else { - render(c, http.StatusOK, mapHolidays(holidays)) + render(c, http.StatusOK, mapHolidays(holidays), search.Type) } } } @@ -54,6 +57,18 @@ type HolidayResponse struct { Holidays []HolidayItemResponse `json:"holidays"` } +func (h HolidayResponse) CSV() []byte { + buffer := bytes.Buffer{} + csvWriter := csv.NewWriter(&buffer) + csvWriter.Write([]string{"id", "date", "name", "description", "isStateHoliday", "isReligiousHoliday"}) + + for _, item := range h.Holidays { + csvWriter.Write([]string{item.Id.String(), item.Date.String(), item.Name, item.Description, strconv.FormatBool(item.IsStateHoliday), strconv.FormatBool(item.IsReligiousHoliday)}) + } + csvWriter.Flush() + return buffer.Bytes() +} + type HolidayItemResponse struct { XMLName xml.Name `json:"-" xml:"Holiday"` Id uuid.UUID `json:"id" xml:"id,attr"` diff --git a/holiday/model.go b/holiday/model.go index b4f42ef..35dbead 100644 --- a/holiday/model.go +++ b/holiday/model.go @@ -34,6 +34,7 @@ type Search struct { RangeEnd *time.Time `form:"range_end" time_format:"2006-01-02"` StateHoliday string `form:"state_holiday,omitempty" binding:"omitempty"` ReligiousHoliday string `form:"religious_holiday,omitempty" binding:"omitempty"` + Type *string `form:"type,omitempty" binding:"omitempty"` } func (s Search) IsStateHoliday() *bool { diff --git a/main.go b/main.go index 1f64b86..7ad4c81 100644 --- a/main.go +++ b/main.go @@ -62,16 +62,20 @@ func main() { setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService) g.GET("/", func(c *gin.Context) { - year := time.Now().Year() - search := holiday.Search{Country: "HR", Year: &year} + search := holiday.Search{Country: "HR", Year: nil} if err := c.ShouldBindQuery(&search); err != nil { c.AbortWithError(http.StatusBadRequest, err) return } - holidays, _ := holidayService.Find(search, holiday.Paging{PageSize: 100}) + countries, _ := countryService.Find() years, _ := yearService.Find() - c.HTML(http.StatusOK, "index.gohtml", gin.H{"Years": years, "Countries": countries, "Search": search, "Holidays": mapHolidays(holidays).Holidays}) + 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() @@ -113,9 +117,11 @@ func loadTemplates(g *gin.Engine) { } return false }, + "importSvg": IncludeHTML, }) g.LoadHTMLFiles( "templates/index.gohtml", + "templates/results.gohtml", "templates/search.gohtml", "templates/documentation.gohtml", @@ -132,6 +138,16 @@ func loadTemplates(g *gin.Engine) { ) } +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())) diff --git a/render.go b/render.go index e6da941..b28a16f 100644 --- a/render.go +++ b/render.go @@ -4,12 +4,34 @@ import ( "github.com/gin-gonic/gin" ) -func render[T any](c *gin.Context, status int, response T) { - switch c.GetHeader("accept") { +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", c.GetHeader("accept")+"; charset=utf-8") + c.Header("content-type", value+"; charset=utf-8; header=present;") c.XML(status, response) case "application/json": fallthrough diff --git a/templates/admin_dashboard.gohtml b/templates/admin_dashboard.gohtml index 06f16cb..0e0739c 100644 --- a/templates/admin_dashboard.gohtml +++ b/templates/admin_dashboard.gohtml @@ -61,7 +61,7 @@
+ {{ importSvg "/assets/icons/search.svg"}} Search @@ -85,11 +85,11 @@ {{$entry.Name}} {{$entry.Date.Format "2006-01-02"}} - - + + - - + + {{end}} diff --git a/templates/countries.gohtml b/templates/countries.gohtml index 6c2fe13..6afb689 100644 --- a/templates/countries.gohtml +++ b/templates/countries.gohtml @@ -46,8 +46,8 @@ {{$entry.IsoName}} {{$entry.Name}} - - + + {{end}} diff --git a/templates/dialogs/check-is-a-holiday.gohtml b/templates/dialogs/check-is-a-holiday.gohtml index 0be8c52..5e1c96d 100644 --- a/templates/dialogs/check-is-a-holiday.gohtml +++ b/templates/dialogs/check-is-a-holiday.gohtml @@ -1,7 +1,7 @@

Is it a holiday?

- +
diff --git a/templates/index.gohtml b/templates/index.gohtml index 7055a89..0c99c47 100644 --- a/templates/index.gohtml +++ b/templates/index.gohtml @@ -1,95 +1,32 @@ - + - - Holiday-api - + + + + + -
-
-
+
-
- -
- -
- - - - - - - - - - - {{range $entry := .Holidays}} - - - - - - - {{end}} - -
NameDateStateReligious
{{$entry.Name}}{{$entry.Date.Format "2006-01-02"}}
-
-
+
+
+ + +
+
+ +
+
+ + \ No newline at end of file diff --git a/templates/results.gohtml b/templates/results.gohtml new file mode 100644 index 0000000..2d20803 --- /dev/null +++ b/templates/results.gohtml @@ -0,0 +1,88 @@ + + + + Holiday-api | {{deferint $.Search.Year}} + + + + + + + + + + +
+
+
+

Holiday-api | {{deferint $.Search.Year}}

+
+
+
+ + +
+
+ +
+
+ +
+
+
+ + + + + + + + + {{range $entry := .Holidays}} + + + + + {{end}} + +
ImeDatum
{{$entry.Name}}{{$entry.Date.Format "02.01.2006."}}
+
+ + \ No newline at end of file diff --git a/templates/search.gohtml b/templates/search.gohtml index 9f51ddc..1080ee3 100644 --- a/templates/search.gohtml +++ b/templates/search.gohtml @@ -60,11 +60,11 @@ Is state holiday: - + Is religious holiday: - +