Compare commits
16 Commits
feature/1_
...
main
Author | SHA1 | Date |
---|---|---|
Borna Rajković | b2646e5aa1 | |
Borna Rajković | b4288b2abb | |
brajkovic | 67c72e8b4f | |
Borna Rajković | c6f365ab28 | |
brajkovic | 2b61f9e0b1 | |
Borna Rajković | 45b220c69f | |
Borna Rajković | 8fbdffc965 | |
Borna Rajković | 76ef62bb96 | |
Borna Rajković | 6bd0c9e37c | |
Borna Rajković | 0173086939 | |
Borna Rajković | 9bdb5dd129 | |
Borna Rajković | 6163c0a04f | |
Borna Rajković | 7a64afd767 | |
Borna Rajković | af2d0f03c6 | |
Borna Rajković | ef526e6ff4 | |
brajkovic | 99c5e5cd13 |
|
@ -1,3 +1,5 @@
|
||||||
.idea/**
|
.idea/**
|
||||||
holiday-api
|
holiday-api
|
||||||
.env
|
.env
|
||||||
|
**/.DS_Store
|
||||||
|
.env*
|
211
README.md
|
@ -1,96 +1,117 @@
|
||||||
# Holiday api
|
# Holiday api
|
||||||
|
|
||||||
Simple api used for tracking holidays
|
Simple api used for tracking holidays
|
||||||
|
|
||||||
## Endpoints
|
To check out application, open [https://holiday.bbr-dev.info](https://holiday.bbr-dev.info)
|
||||||
|
|
||||||
Fetching is done via `GET /api/v1/holidays` endpoint
|
## Endpoints
|
||||||
|
|
||||||
That endpoint accepts a list of required and optional parameters
|
Fetching is done via `GET /api/v1/holidays` endpoint
|
||||||
|
|
||||||
### Required parameters
|
That endpoint accepts a list of required and optional parameters
|
||||||
`country`
|
|
||||||
- determines for which country holidays are fetched, uses ISO 3166-1 Alpha-2 codes [more info here](https://en.wikipedia.org/wiki/ISO_3166-1)
|
### Required parameters
|
||||||
- eg. `country=US`
|
`country`
|
||||||
|
- determines for which country holidays are fetched, uses ISO 3166-1 Alpha-2 codes [more info here](https://en.wikipedia.org/wiki/ISO_3166-1)
|
||||||
### Optional parameters
|
- eg. `country=US`
|
||||||
`year`
|
|
||||||
- returns only holidays for given year
|
### Optional parameters
|
||||||
- eg. `year=2023`
|
`year`
|
||||||
- only used if no more important parameters are defined
|
- returns only holidays for given year
|
||||||
|
- eg. `year=2023`
|
||||||
`date`
|
- only used if no more important parameters are defined
|
||||||
- returns only holidays for given date
|
|
||||||
- date must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html)
|
`date`
|
||||||
- eg. `date=2021-12-25`
|
- returns only holidays for given date
|
||||||
- if defined year and rangeStart|rangeEnd parameters are ignored
|
- date must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html)
|
||||||
|
- eg. `date=2021-12-25`
|
||||||
|
- if defined year and rangeStart|rangeEnd parameters are ignored
|
||||||
`rangeStart|rangeEnd`
|
|
||||||
- returns holidays in given range with both ends being inclusive
|
|
||||||
- if either limit isn't defined it is assumed to be up to or all from given limit (if rangeStart isn't defined all holidays before rangeEnd are returned and vice-verse)
|
`range_start|range_end`
|
||||||
- dates must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html)
|
- returns holidays in given range with both ends being inclusive
|
||||||
- eg. `rangeStart=2021-12-25&rangeEnd=2023-01-23`, `rangeStart=2023-01-20`
|
- if either limit isn't defined it is assumed to be up to or all from given limit (if rangeStart isn't defined all holidays before rangeEnd are returned and vice-verse)
|
||||||
- if defined year parameter is ignored
|
- dates must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html)
|
||||||
|
- eg. `range_start=2021-12-25&range_end=2023-01-23`, `range_start=2023-01-20`
|
||||||
`stateHoliday`
|
- if defined year parameter is ignored
|
||||||
- if set true only holidays that are tagged as state holidays are returned, similar for if set false, if not set all holidays are returned
|
|
||||||
- eg. `stateHoliday=true`, `stateHoliday=false`
|
`state_holiday`
|
||||||
|
- if set true only holidays that are tagged as state holidays are returned, similar for if set false, if not set all holidays are returned
|
||||||
`religiousHoliday`
|
- eg. `state_holiday=true`, `state_holiday=false`
|
||||||
- if set true only holidays that are tagged as religious holidays are returned, similar for if set false, if not set all holidays are returned
|
|
||||||
- eg. `religiousHoliday=true`, `religiousHoliday=false`
|
`religious_holiday`
|
||||||
|
- if set true only holidays that are tagged as religious holidays are returned, similar for if set false, if not set all holidays are returned
|
||||||
#### Paging
|
- eg. `religious_holiday=true`, `religious_holiday=false`
|
||||||
`pageSize`
|
|
||||||
- returns at most pageSize number of holidays
|
#### Paging
|
||||||
- eg. `pageSize=20`
|
`page_size`
|
||||||
- only applied if page is defined as well, by default set to 20
|
- returns at most pageSize number of holidays
|
||||||
|
- eg. `page_size=20`
|
||||||
`page`
|
- only applied if page is defined as well, by default set to 20
|
||||||
- returns nth page of holidays, paging starts at 0
|
|
||||||
- eg. `page=0`
|
`page`
|
||||||
|
- returns nth page of holidays, paging starts at 0
|
||||||
## Response
|
- eg. `page=0`
|
||||||
|
|
||||||
By default, responses are returned as a json array
|
## Response
|
||||||
```
|
|
||||||
[{
|
By default, responses are returned as a json array
|
||||||
id: string;
|
```
|
||||||
date: string(ISO 8601);
|
{
|
||||||
name: string;
|
holidays: [{
|
||||||
description: string;
|
id: string;
|
||||||
isStateHoliday: boolean;
|
date: string(ISO 8601);
|
||||||
isReligiousHoliday: boolean;
|
name: string;
|
||||||
},...]
|
description: string;
|
||||||
```
|
isStateHoliday: boolean;
|
||||||
eg.
|
isReligiousHoliday: boolean;
|
||||||
```
|
},...]
|
||||||
[{
|
}
|
||||||
"id": "74a2a769-abf2-45d4-bdc4-442bbcc89138",
|
```
|
||||||
"date": "2023-12-25",
|
e.g.
|
||||||
"name": "Christmas",
|
```
|
||||||
"description": "TBD",
|
{
|
||||||
"isStateHoliday": true,
|
"holidays": [{
|
||||||
"isReligiousHoliday": true
|
"id": "74a2a769-abf2-45d4-bdc4-442bbcc89138",
|
||||||
}]
|
"date": "2023-12-25",
|
||||||
```
|
"name": "Christmas",
|
||||||
|
"description": "TBD",
|
||||||
But can be returned as XML or CSV by setting appropriate `Accept` header (application/xml, text/xml or text/csv)
|
"isStateHoliday": true,
|
||||||
|
"isReligiousHoliday": true
|
||||||
XML Response
|
}]
|
||||||
```
|
}
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
```
|
||||||
<Holidays>
|
|
||||||
<Holiday id="74a2a769-abf2-45d4-bdc4-442bbcc89138" date="2023-12-25" isReligious="true" isState="true">
|
But can be returned as XML or CSV by setting appropriate `Accept` header (application/xml, text/xml or text/csv)
|
||||||
<name>Christmas</name>
|
|
||||||
<description>TBD</description>
|
XML Response
|
||||||
</Holiday>
|
```
|
||||||
</Holidays>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||||
```
|
<Holidays>
|
||||||
|
<Holiday id="74a2a769-abf2-45d4-bdc4-442bbcc89138" date="2023-12-25" isReligious="true" isState="true">
|
||||||
CSV Response
|
<name>Christmas</name>
|
||||||
```
|
<description>TBD</description>
|
||||||
id,date,name,description,is_state_holiday,is_religious_holiday
|
</Holiday>
|
||||||
74a2a769-abf2-45d4-bdc4-442bbcc89138,2023-12-25,Christmas,TBD,true,true
|
</Holidays>
|
||||||
|
```
|
||||||
|
|
||||||
|
CSV Response
|
||||||
|
```
|
||||||
|
id,date,name,description,is_state_holiday,is_religious_holiday
|
||||||
|
74a2a769-abf2-45d4-bdc4-442bbcc89138,2023-12-25,Christmas,TBD,true,true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
To start server few environment variables need to be set up. This can be done by creating `.env` file with following content
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PSQL_HOST=localhost
|
||||||
|
PSQL_PORT=5432
|
||||||
|
PSQL_USER=holiday
|
||||||
|
PSQL_PASSWORD=holidayPassword
|
||||||
|
PSQL_DB=holiday
|
||||||
|
|
||||||
|
PROFILE=dev,basic-auth
|
||||||
|
AUTH_KEY=holiday:holidayPassword
|
||||||
```
|
```
|
|
@ -0,0 +1,350 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"holiday-api/domain/holiday"
|
||||||
|
"log/slog"
|
||||||
|
"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) {
|
||||||
|
slog.Error(message, slog.String("err", errorMessage(err)))
|
||||||
|
c.AbortWithError(statusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorMessage(err error) string {
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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",
|
||||||
|
)
|
||||||
|
}
|
After Width: | Height: | Size: 633 KiB |
After Width: | Height: | Size: 236 KiB |
|
@ -11,17 +11,15 @@ window.addEventListener('load', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateQuery(query) {
|
function generateQuery(query) {
|
||||||
initialQuery = `curl -H "accept: ${query['contentType']}" "https://holiday.bbr-dev.info/api/v1/holidays?country=${query['country']}`
|
initialQuery = `https://holiday.bbr-dev.info/api/v1/holidays?country=${query['country']}`
|
||||||
|
|
||||||
delete query['contentType'];
|
|
||||||
delete query['country'];
|
delete query['country'];
|
||||||
|
|
||||||
for(const key in query) {
|
for(const key in query) {
|
||||||
initialQuery += `&${key}=${query[key]}`
|
initialQuery += `&${key}=${query[key]}`
|
||||||
}
|
}
|
||||||
initialQuery+='"';
|
document.querySelector('#query-link').href = initialQuery;
|
||||||
|
document.querySelector('#result-content').innerHTML = "curl \"" + initialQuery + "\"";
|
||||||
document.querySelector('#result-content').innerHTML = initialQuery;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let country = document.querySelector("#country");
|
let country = document.querySelector("#country");
|
||||||
|
@ -36,10 +34,8 @@ window.addEventListener('load', () => {
|
||||||
let dDate = document.querySelector("#dsd-date");
|
let dDate = document.querySelector("#dsd-date");
|
||||||
|
|
||||||
let dStartRange = document.querySelector("#dsr-start-range");
|
let dStartRange = document.querySelector("#dsr-start-range");
|
||||||
let dStartRangeRequired = document.querySelector("#dsr-start-range-required");
|
|
||||||
|
|
||||||
let dEndRange = document.querySelector("#dsr-end-range");
|
let dEndRange = document.querySelector("#dsr-end-range");
|
||||||
let dEndRangeRequired = document.querySelector("#dsr-end-range-required");
|
|
||||||
|
|
||||||
let queryGenerator = document.querySelector("#query-generator");
|
let queryGenerator = document.querySelector("#query-generator");
|
||||||
|
|
||||||
|
@ -48,11 +44,12 @@ window.addEventListener('load', () => {
|
||||||
query['country'] = country.value;
|
query['country'] = country.value;
|
||||||
query['contentType'] = contentType.value;
|
query['contentType'] = contentType.value;
|
||||||
if(stateHoliday.value === 'true' || stateHoliday.value === 'false') {
|
if(stateHoliday.value === 'true' || stateHoliday.value === 'false') {
|
||||||
query['stateHoliday'] = parseBoolean(stateHoliday.value);
|
query['state_holiday'] = parseBoolean(stateHoliday.value);
|
||||||
}
|
}
|
||||||
if(religiousHoliday.value === 'true' || religiousHoliday.value === 'false') {
|
if(religiousHoliday.value === 'true' || religiousHoliday.value === 'false') {
|
||||||
query['religiousHoliday'] = parseBoolean(religiousHoliday.value);
|
query['religious_holiday'] = parseBoolean(religiousHoliday.value);
|
||||||
}
|
}
|
||||||
|
console.log(dateSelector.value, dStartRange.value, dEndRange.value);
|
||||||
switch(dateSelector.value) {
|
switch(dateSelector.value) {
|
||||||
case 'year':
|
case 'year':
|
||||||
query['year'] = dYear.value;
|
query['year'] = dYear.value;
|
||||||
|
@ -63,11 +60,11 @@ window.addEventListener('load', () => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'range':
|
case 'range':
|
||||||
if(dStartRangeRequired.checked && dStartRange.value) {
|
if(dStartRange.value) {
|
||||||
query['startRange'] = dStartRange.value;
|
query['range_start'] = dStartRange.value;
|
||||||
}
|
}
|
||||||
if(dEndRangeRequired.checked && dEndRange.value) {
|
if(dEndRange.value) {
|
||||||
query['endRange'] = dEndRange.value;
|
query['range_end'] = dEndRange.value;
|
||||||
}
|
}
|
||||||
case 'all':
|
case 'all':
|
||||||
default:
|
default:
|
||||||
|
@ -79,13 +76,14 @@ window.addEventListener('load', () => {
|
||||||
queryGenerator.addEventListener('change', event => {
|
queryGenerator.addEventListener('change', event => {
|
||||||
const query = {};
|
const query = {};
|
||||||
query['country'] = country.value;
|
query['country'] = country.value;
|
||||||
query['contentType'] = contentType.value;
|
query['type'] = contentType.value;
|
||||||
if(stateHoliday.value === 'true' || stateHoliday.value === 'false') {
|
if(stateHoliday.value === 'true' || stateHoliday.value === 'false') {
|
||||||
query['stateHoliday'] = parseBoolean(stateHoliday.value);
|
query['state_holiday'] = parseBoolean(stateHoliday.value);
|
||||||
}
|
}
|
||||||
if(religiousHoliday.value === 'true' || religiousHoliday.value === 'false') {
|
if(religiousHoliday.value === 'true' || religiousHoliday.value === 'false') {
|
||||||
query['religiousHoliday'] = parseBoolean(religiousHoliday.value);
|
query['religious_holiday'] = parseBoolean(religiousHoliday.value);
|
||||||
}
|
}
|
||||||
|
console.log(dateSelector.value, dStartRange.value, dEndRange.value);
|
||||||
switch(dateSelector.value) {
|
switch(dateSelector.value) {
|
||||||
case 'year':
|
case 'year':
|
||||||
query['year'] = dYear.value;
|
query['year'] = dYear.value;
|
||||||
|
@ -96,11 +94,11 @@ window.addEventListener('load', () => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'range':
|
case 'range':
|
||||||
if(dStartRangeRequired.checked && dStartRange.value) {
|
if(dStartRange.value) {
|
||||||
query['startRange'] = dStartRange.value;
|
query['range_start'] = dStartRange.value;
|
||||||
}
|
}
|
||||||
if(dEndRangeRequired.checked && dEndRange.value) {
|
if(dEndRange.value) {
|
||||||
query['endRange'] = dEndRange.value;
|
query['range_end'] = dEndRange.value;
|
||||||
}
|
}
|
||||||
case 'all':
|
case 'all':
|
||||||
default:
|
default:
|
||||||
|
|
After Width: | Height: | Size: 766 B |
|
@ -0,0 +1,482 @@
|
||||||
|
body {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background-color: #f3ebe6;
|
||||||
|
--background: #ffffff;
|
||||||
|
--color: #000;
|
||||||
|
--border: #dddddd;
|
||||||
|
|
||||||
|
--form-label: #999;
|
||||||
|
--form-border: #bbb;
|
||||||
|
--form-focus: #333;
|
||||||
|
|
||||||
|
--dialog-border: #5f9ea07f;
|
||||||
|
--dialog-backdrop: #979f9f63;
|
||||||
|
|
||||||
|
--dropdown-background: #f0f0f0;
|
||||||
|
|
||||||
|
--primary: #5f9ea0;
|
||||||
|
--primary-contrast: #ffffff;
|
||||||
|
--primary-accent: #6a7579;
|
||||||
|
|
||||||
|
--code-background: #aaa;
|
||||||
|
--code-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-color: #f3ebe6;
|
||||||
|
--background: #333;
|
||||||
|
--color: #f0f0f0;
|
||||||
|
--border: #666;
|
||||||
|
|
||||||
|
--form-label: #999;
|
||||||
|
--form-border: #555;
|
||||||
|
--form-focus: #ccc;
|
||||||
|
|
||||||
|
--dialog-border: rgba(16, 173, 173, 0.5);
|
||||||
|
--dialog-backdrop: rgba(1, 70, 70, 0.39);
|
||||||
|
|
||||||
|
--dropdown-background: #222;
|
||||||
|
|
||||||
|
--primary: #0e8d93;
|
||||||
|
--primary-contrast: #333;
|
||||||
|
--primary-accent: #08363a;
|
||||||
|
|
||||||
|
--code-background: #444;
|
||||||
|
--code-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
color: var(--color);
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main {
|
||||||
|
background: var(--background-color);
|
||||||
|
width: 640px;
|
||||||
|
max-width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 1em;
|
||||||
|
background: var(--background);
|
||||||
|
position: sticky;
|
||||||
|
z-index: 10;
|
||||||
|
top: 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--background);
|
||||||
|
margin: auto;
|
||||||
|
padding: 1em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (min-width: 480px) {
|
||||||
|
.dialog {
|
||||||
|
padding: 1em 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
min-width: 100vw;
|
||||||
|
min-height: 100dvh;
|
||||||
|
|
||||||
|
max-width: 100vw;
|
||||||
|
max-height: 100dvh;
|
||||||
|
|
||||||
|
background: url("/assets/background.jpg");
|
||||||
|
background-position: bottom;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.background-image {
|
||||||
|
min-width: 100vw;
|
||||||
|
min-height: 100dvh;
|
||||||
|
|
||||||
|
max-width: 100vw;
|
||||||
|
max-height: 100dvh;
|
||||||
|
background: url("/assets/background-dark.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: 1.1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h1 a {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 480px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
.form-field label {
|
||||||
|
display: block;
|
||||||
|
color: var(--form-label);
|
||||||
|
}
|
||||||
|
.form-field input, .form-field textarea {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--form-border);
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field .checkbox {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field input[type=checkbox] {
|
||||||
|
width: inherit;
|
||||||
|
margin-left: 1em;
|
||||||
|
accent-color: var(--primary-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field input:focus, .form-field textarea:focus {
|
||||||
|
color: var(--primary);
|
||||||
|
border-bottom: 1px solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field input:not(focus), .form-field textarea:not(focus) {
|
||||||
|
color: var(--form-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field select {
|
||||||
|
all: unset;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--form-border);
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field select:focus {
|
||||||
|
color: var(--primary);
|
||||||
|
border-bottom: 1px solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field select:not(focus) {
|
||||||
|
color: var(--form-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field:focus-within label {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.primary, button.primary {
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
background-color: var(--primary);
|
||||||
|
font-family: Quicksand, Arial, sans-serif;
|
||||||
|
border: none;
|
||||||
|
color: var(--primary-contrast);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary, button.secondary {
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
background-color: var(--primary-contrast);
|
||||||
|
font-family: Quicksand, Arial, sans-serif;
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.primary, button.primary:hover {
|
||||||
|
filter: contrast(140%);
|
||||||
|
}
|
||||||
|
.button.secondary:hover, button.secondary:hover {
|
||||||
|
filter: contrast(80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.button.primary, button.primary:hover {
|
||||||
|
filter: brightness(140%);
|
||||||
|
}
|
||||||
|
.button.secondary:hover, button.secondary:hover {
|
||||||
|
filter: brightness(140%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*index.html*/
|
||||||
|
|
||||||
|
.search-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 480px) {
|
||||||
|
.search-dialog {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 10vw;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 380px;
|
||||||
|
padding-top: 40px;
|
||||||
|
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;
|
||||||
|
margin-top: 1em;
|
||||||
|
width: 600px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.results td {
|
||||||
|
padding: 0.4em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*documentation.html*/
|
||||||
|
.documentation-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 480px) {
|
||||||
|
.documentation-dialog {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 1em;
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentation-dialog #result-content {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
background: var(--code-background);
|
||||||
|
color: var(--code-color);
|
||||||
|
padding: 1em;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.radio-group {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.radio-group input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.radio-group div label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
border: none;
|
||||||
|
color: var(--color);
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.radio-group div label {
|
||||||
|
min-width: 3ch;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
section.radio-group div label:first-of-type {
|
||||||
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-bottom-left-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.radio-group div label:last-of-type {
|
||||||
|
border-top-right-radius: 0.25rem;
|
||||||
|
border-bottom-right-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.radio-group div input:checked+label {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--primary-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentation-dialog .range-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.documentation-dialog .range-selector section {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentation-dialog .hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 1em;
|
||||||
|
gap: 1em;
|
||||||
|
background: transparent;
|
||||||
|
width: calc(100% - 2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-dashboard .filter-dialog {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-dashboard .table-results table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.admin-dashboard .table-results table th, .admin-dashboard .table-results table td {
|
||||||
|
text-align: left;
|
||||||
|
margin: 0.4em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-dashboard .table-results table tr td:nth-child(3) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (min-width: 480px) {
|
||||||
|
.admin-dashboard .filter-dialog {
|
||||||
|
width: 300px;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-icon {
|
||||||
|
mask: url(/assets/icons/edit.svg) no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
background: var(--color);
|
||||||
|
}
|
||||||
|
.delete-icon {
|
||||||
|
mask: url(/assets/icons/trash-delete.svg) no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
background: var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
|
img.icon, button.icon svg, span.icon {
|
||||||
|
height: 1.2em;
|
||||||
|
width: 1.2em;
|
||||||
|
}
|
||||||
|
button.icon, a.icon {
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-content: baseline;
|
||||||
|
gap: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.icon svg, button.icon svg * {
|
||||||
|
fill: var(--primary-contrast);
|
||||||
|
color: var(--primary-contrast);
|
||||||
|
stroke: var(--primary-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
border: 1px solid var(--dialog-border);
|
||||||
|
}
|
||||||
|
dialog::backdrop {
|
||||||
|
background: var(--dialog-backdrop);
|
||||||
|
}
|
||||||
|
dialog {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 80vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
|
@ -1,6 +1,16 @@
|
||||||
const dialogContainerId = "#dialog-container"
|
const dialogContainerId = "#dialog-container"
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
|
document.querySelectorAll(".dropdown").forEach(el => {
|
||||||
|
const action = el.querySelector(".dropdown-action");
|
||||||
|
const content = el.querySelector(".dropdown-content");
|
||||||
|
|
||||||
|
action.addEventListener('click', () => {
|
||||||
|
content.classList.toggle('selected');
|
||||||
|
content.style.marginLeft = `-${(content.offsetWidth - action.offsetWidth)}px`;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// configure radio button groups
|
// configure radio button groups
|
||||||
document.querySelectorAll("section.radio-group").forEach(el => {
|
document.querySelectorAll("section.radio-group").forEach(el => {
|
||||||
const submittedInput = el.querySelector("input[type=hidden]");
|
const submittedInput = el.querySelector("input[type=hidden]");
|
||||||
|
@ -23,7 +33,13 @@ window.addEventListener('load', () => {
|
||||||
let response = await fetch(btn.dataset.url);
|
let response = await fetch(btn.dataset.url);
|
||||||
if(response.ok) {
|
if(response.ok) {
|
||||||
response = await response.text();
|
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;
|
container.innerHTML = response;
|
||||||
const dialogReference = document.querySelector(selector);
|
const dialogReference = document.querySelector(selector);
|
||||||
dialogReference?.showModal();
|
dialogReference?.showModal();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8V11H8C7.44772 11 7 11.4477 7 12C7 12.5523 7.44772 13 8 13H11V16C11 16.5523 11.4477 17 12 17C12.5523 17 13 16.5523 13 16V13H16C16.5523 13 17 12.5523 17 12C17 11.4477 16.5523 11 16 11H13V8Z" fill="#0F1729"/>
|
<path d="M13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8V11H8C7.44772 11 7 11.4477 7 12C7 12.5523 7.44772 13 8 13H11V16C11 16.5523 11.4477 17 12 17C12.5523 17 13 16.5523 13 16V13H16C16.5523 13 17 12.5523 17 12C17 11.4477 16.5523 11 16 11H13V8Z" fill="#0F1729"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12Z" fill="#0F1729"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12Z" fill="#0F1729"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 795 B After Width: | Height: | Size: 791 B |
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#0F1729"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="#0F1729"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 807 B After Width: | Height: | Size: 804 B |
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7071 6.29289C20.0976 6.68342 20.0976 7.31658 19.7071 7.70711L10.4142 17C9.63316 17.7811 8.36683 17.781 7.58579 17L3.29289 12.7071C2.90237 12.3166 2.90237 11.6834 3.29289 11.2929C3.68342 10.9024 4.31658 10.9024 4.70711 11.2929L9 15.5858L18.2929 6.29289C18.6834 5.90237 19.3166 5.90237 19.7071 6.29289Z" fill="#0F1729"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7071 6.29289C20.0976 6.68342 20.0976 7.31658 19.7071 7.70711L10.4142 17C9.63316 17.7811 8.36683 17.781 7.58579 17L3.29289 12.7071C2.90237 12.3166 2.90237 11.6834 3.29289 11.2929C3.68342 10.9024 4.31658 10.9024 4.70711 11.2929L9 15.5858L18.2929 6.29289C18.6834 5.90237 19.3166 5.90237 19.7071 6.29289Z" fill="#0F1729"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 602 B After Width: | Height: | Size: 599 B |
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1739 3.5968C13.8662 3.2047 14.686 3.10369 15.4528 3.31598C15.7928 3.41011 16.0833 3.57409 16.3571 3.7593C16.6172 3.9352 16.9155 4.16808 17.2613 4.43799L17.3117 4.47737C17.6575 4.74728 17.9559 4.98016 18.1897 5.18977C18.4358 5.41046 18.6654 5.65248 18.8393 5.95945C19.2314 6.65177 19.3324 7.47151 19.1201 8.23831C19.026 8.5783 18.862 8.86883 18.6768 9.14267C18.5009 9.40276 18.268 9.70112 17.998 10.0469L10.8953 19.1462C10.8773 19.1692 10.8596 19.1919 10.8421 19.2144C10.5087 19.6419 10.2566 19.9651 9.9445 20.2306C9.68036 20.4553 9.38811 20.6447 9.07512 20.794C8.70535 20.9704 8.30733 21.0685 7.78084 21.1983C7.75324 21.2051 7.72528 21.212 7.69696 21.219L5.57214 21.7435C5.42499 21.7799 5.25702 21.8215 5.10885 21.8442C4.94367 21.8696 4.68789 21.8926 4.40539 21.8022C4.06579 21.6934 3.77603 21.4672 3.58809 21.1642C3.43175 20.9121 3.39197 20.6584 3.3765 20.492C3.36262 20.3427 3.36213 20.1697 3.3617 20.0181C3.36167 20.0087 3.36165 19.9994 3.36162 19.9902L3.35475 17.8295C3.35465 17.8003 3.35455 17.7715 3.35445 17.7431C3.3525 17.2009 3.35103 16.7909 3.4324 16.3894C3.50128 16.0495 3.61406 15.72 3.76791 15.4093C3.94967 15.0421 4.20204 14.7191 4.53586 14.2918C4.55336 14.2694 4.57109 14.2467 4.58905 14.2237L11.6918 5.12435C11.9617 4.77856 12.1946 4.48019 12.4042 4.2464C12.6249 4.00025 12.8669 3.77065 13.1739 3.5968ZM14.9191 5.24347C14.6635 5.17271 14.3903 5.20638 14.1595 5.33708C14.1203 5.35928 14.0459 5.41135 13.8934 5.5815C13.7348 5.75836 13.5438 6.00211 13.2487 6.38018L16.4018 8.84145C16.697 8.46338 16.887 8.21896 17.0201 8.02221C17.1482 7.83291 17.1806 7.74808 17.1926 7.70467C17.2634 7.44907 17.2297 7.17583 17.099 6.94505C17.0768 6.90586 17.0247 6.83145 16.8546 6.6789C16.6777 6.52033 16.434 6.32938 16.0559 6.03426C15.6778 5.73914 15.4334 5.54904 15.2367 5.41597C15.0474 5.28794 14.9625 5.25549 14.9191 5.24347ZM15.1712 10.418L12.0181 7.95674L6.16561 15.4543C5.75585 15.9792 5.6403 16.135 5.56031 16.2966C5.48339 16.452 5.42699 16.6167 5.39256 16.7866C5.35675 16.9633 5.35262 17.1572 5.35474 17.8231L5.36082 19.7357L7.2176 19.2773C7.86411 19.1177 8.05119 19.0666 8.21391 18.9889C8.37041 18.9143 8.51653 18.8196 8.64861 18.7072C8.78593 18.5904 8.90897 18.4405 9.31872 17.9156L15.1712 10.418ZM12 21C12 20.4477 12.4477 20 13 20H20C20.5523 20 21 20.4477 21 21C21 21.5523 20.5523 22 20 22H13C12.4477 22 12 21.5523 12 21Z" fill="#0F1729"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1739 3.5968C13.8662 3.2047 14.686 3.10369 15.4528 3.31598C15.7928 3.41011 16.0833 3.57409 16.3571 3.7593C16.6172 3.9352 16.9155 4.16808 17.2613 4.43799L17.3117 4.47737C17.6575 4.74728 17.9559 4.98016 18.1897 5.18977C18.4358 5.41046 18.6654 5.65248 18.8393 5.95945C19.2314 6.65177 19.3324 7.47151 19.1201 8.23831C19.026 8.5783 18.862 8.86883 18.6768 9.14267C18.5009 9.40276 18.268 9.70112 17.998 10.0469L10.8953 19.1462C10.8773 19.1692 10.8596 19.1919 10.8421 19.2144C10.5087 19.6419 10.2566 19.9651 9.9445 20.2306C9.68036 20.4553 9.38811 20.6447 9.07512 20.794C8.70535 20.9704 8.30733 21.0685 7.78084 21.1983C7.75324 21.2051 7.72528 21.212 7.69696 21.219L5.57214 21.7435C5.42499 21.7799 5.25702 21.8215 5.10885 21.8442C4.94367 21.8696 4.68789 21.8926 4.40539 21.8022C4.06579 21.6934 3.77603 21.4672 3.58809 21.1642C3.43175 20.9121 3.39197 20.6584 3.3765 20.492C3.36262 20.3427 3.36213 20.1697 3.3617 20.0181C3.36167 20.0087 3.36165 19.9994 3.36162 19.9902L3.35475 17.8295C3.35465 17.8003 3.35455 17.7715 3.35445 17.7431C3.3525 17.2009 3.35103 16.7909 3.4324 16.3894C3.50128 16.0495 3.61406 15.72 3.76791 15.4093C3.94967 15.0421 4.20204 14.7191 4.53586 14.2918C4.55336 14.2694 4.57109 14.2467 4.58905 14.2237L11.6918 5.12435C11.9617 4.77856 12.1946 4.48019 12.4042 4.2464C12.6249 4.00025 12.8669 3.77065 13.1739 3.5968ZM14.9191 5.24347C14.6635 5.17271 14.3903 5.20638 14.1595 5.33708C14.1203 5.35928 14.0459 5.41135 13.8934 5.5815C13.7348 5.75836 13.5438 6.00211 13.2487 6.38018L16.4018 8.84145C16.697 8.46338 16.887 8.21896 17.0201 8.02221C17.1482 7.83291 17.1806 7.74808 17.1926 7.70467C17.2634 7.44907 17.2297 7.17583 17.099 6.94505C17.0768 6.90586 17.0247 6.83145 16.8546 6.6789C16.6777 6.52033 16.434 6.32938 16.0559 6.03426C15.6778 5.73914 15.4334 5.54904 15.2367 5.41597C15.0474 5.28794 14.9625 5.25549 14.9191 5.24347ZM15.1712 10.418L12.0181 7.95674L6.16561 15.4543C5.75585 15.9792 5.6403 16.135 5.56031 16.2966C5.48339 16.452 5.42699 16.6167 5.39256 16.7866C5.35675 16.9633 5.35262 17.1572 5.35474 17.8231L5.36082 19.7357L7.2176 19.2773C7.86411 19.1177 8.05119 19.0666 8.21391 18.9889C8.37041 18.9143 8.51653 18.8196 8.64861 18.7072C8.78593 18.5904 8.90897 18.4405 9.31872 17.9156L15.1712 10.418ZM12 21C12 20.4477 12.4477 20 13 20H20C20.5523 20 21 20.4477 21 21C21 21.5523 20.5523 22 20 22H13C12.4477 22 12 21.5523 12 21Z" fill="#0F1729"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 5C7.68629 5 5 7.68629 5 11C5 14.3137 7.68629 17 11 17C14.3137 17 17 14.3137 17 11C17 7.68629 14.3137 5 11 5ZM3 11C3 6.58172 6.58172 3 11 3C15.4183 3 19 6.58172 19 11C19 12.8487 18.3729 14.551 17.3199 15.9056L20.7071 19.2929C21.0976 19.6834 21.0976 20.3166 20.7071 20.7071C20.3166 21.0976 19.6834 21.0976 19.2929 20.7071L15.9056 17.3199C14.551 18.3729 12.8487 19 11 19C6.58172 19 3 15.4183 3 11Z" fill="#0F1729"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 5C7.68629 5 5 7.68629 5 11C5 14.3137 7.68629 17 11 17C14.3137 17 17 14.3137 17 11C17 7.68629 14.3137 5 11 5ZM3 11C3 6.58172 6.58172 3 11 3C15.4183 3 19 6.58172 19 11C19 12.8487 18.3729 14.551 17.3199 15.9056L20.7071 19.2929C21.0976 19.6834 21.0976 20.3166 20.7071 20.7071C20.3166 21.0976 19.6834 21.0976 19.2929 20.7071L15.9056 17.3199C14.551 18.3729 12.8487 19 11 19C6.58172 19 3 15.4183 3 11Z" fill="#0F1729"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 693 B |
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.10002 5H3C2.44772 5 2 5.44772 2 6C2 6.55228 2.44772 7 3 7H4.06055L4.88474 20.1871C4.98356 21.7682 6.29471 23 7.8789 23H16.1211C17.7053 23 19.0164 21.7682 19.1153 20.1871L19.9395 7H21C21.5523 7 22 6.55228 22 6C22 5.44772 21.5523 5 21 5H19.0073C19.0018 4.99995 18.9963 4.99995 18.9908 5H16.9C16.4367 2.71776 14.419 1 12 1C9.58104 1 7.56329 2.71776 7.10002 5ZM9.17071 5H14.8293C14.4175 3.83481 13.3062 3 12 3C10.6938 3 9.58254 3.83481 9.17071 5ZM17.9355 7H6.06445L6.88085 20.0624C6.91379 20.5894 7.35084 21 7.8789 21H16.1211C16.6492 21 17.0862 20.5894 17.1192 20.0624L17.9355 7ZM14.279 10.0097C14.83 10.0483 15.2454 10.5261 15.2068 11.0771L14.7883 17.0624C14.7498 17.6134 14.2719 18.0288 13.721 17.9903C13.17 17.9517 12.7546 17.4739 12.7932 16.9229L13.2117 10.9376C13.2502 10.3866 13.7281 9.97122 14.279 10.0097ZM9.721 10.0098C10.2719 9.97125 10.7498 10.3866 10.7883 10.9376L11.2069 16.923C11.2454 17.4739 10.83 17.9518 10.2791 17.9903C9.72811 18.0288 9.25026 17.6134 9.21173 17.0625L8.79319 11.0771C8.75467 10.5262 9.17006 10.0483 9.721 10.0098Z" fill="#0F1729"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.10002 5H3C2.44772 5 2 5.44772 2 6C2 6.55228 2.44772 7 3 7H4.06055L4.88474 20.1871C4.98356 21.7682 6.29471 23 7.8789 23H16.1211C17.7053 23 19.0164 21.7682 19.1153 20.1871L19.9395 7H21C21.5523 7 22 6.55228 22 6C22 5.44772 21.5523 5 21 5H19.0073C19.0018 4.99995 18.9963 4.99995 18.9908 5H16.9C16.4367 2.71776 14.419 1 12 1C9.58104 1 7.56329 2.71776 7.10002 5ZM9.17071 5H14.8293C14.4175 3.83481 13.3062 3 12 3C10.6938 3 9.58254 3.83481 9.17071 5ZM17.9355 7H6.06445L6.88085 20.0624C6.91379 20.5894 7.35084 21 7.8789 21H16.1211C16.6492 21 17.0862 20.5894 17.1192 20.0624L17.9355 7ZM14.279 10.0097C14.83 10.0483 15.2454 10.5261 15.2068 11.0771L14.7883 17.0624C14.7498 17.6134 14.2719 18.0288 13.721 17.9903C13.17 17.9517 12.7546 17.4739 12.7932 16.9229L13.2117 10.9376C13.2502 10.3866 13.7281 9.97122 14.279 10.0097ZM9.721 10.0098C10.2719 9.97125 10.7498 10.3866 10.7883 10.9376L11.2069 16.923C11.2454 17.4739 10.83 17.9518 10.2791 17.9903C9.72811 18.0288 9.25026 17.6134 9.21173 17.0625L8.79319 11.0771C8.75467 10.5262 9.17006 10.0483 9.721 10.0098Z" fill="#0F1729"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
276
assets/style.css
|
@ -1,276 +0,0 @@
|
||||||
/* cleanup */
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
width: 100%;
|
|
||||||
background: #666;
|
|
||||||
}
|
|
||||||
header h1 {
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
header a {
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
width: 100%;
|
|
||||||
background: #f7f7f7;
|
|
||||||
}
|
|
||||||
nav a, nav button {
|
|
||||||
display: inline-block;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1rem;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
transition: ease-out 0.2s;
|
|
||||||
}
|
|
||||||
nav a:hover, nav button:hover {
|
|
||||||
background: #e0e0e0;
|
|
||||||
color: #000;
|
|
||||||
transition: ease-out 0.2s;
|
|
||||||
}
|
|
||||||
nav a.selected, nav button.selected {
|
|
||||||
background: #216897;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
table:not(.clean) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table th, table td {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
table thead * {
|
|
||||||
background: #666;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
table th, table td {
|
|
||||||
padding: 0.4rem 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
table:not(.clean) tbody tr:nth-child(2n+1) {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
table:not(.clean) tbody tr:nth-child(2n) {
|
|
||||||
background: #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 0.25rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 1em;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 0.2rem;
|
|
||||||
border: 1px solid #bbb;
|
|
||||||
}
|
|
||||||
.card .card-title {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
.card p {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
form section {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* styling */
|
|
||||||
form section:not(.radio-group) label+:not(select) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
form section {
|
|
||||||
padding-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
form section > input {
|
|
||||||
width: 200px;
|
|
||||||
padding: 0.2em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
form section > textarea {
|
|
||||||
width: 200px;
|
|
||||||
padding: 0.2em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
form section select {
|
|
||||||
display: block;
|
|
||||||
width: 200px;
|
|
||||||
border: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 0.2em 0.5em;
|
|
||||||
background: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
form section label.checkbox {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
form section input[type=checkbox] {
|
|
||||||
vertical-align: middle;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
section.radio-group input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.radio-group div label {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
background: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.radio-group div label {
|
|
||||||
min-width: 64px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.radio-group div label:first-of-type {
|
|
||||||
border-top-left-radius: 0.25rem;
|
|
||||||
border-bottom-left-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.radio-group div label:last-of-type {
|
|
||||||
border-top-right-radius: 0.25rem;
|
|
||||||
border-bottom-right-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.radio-group div input:checked+label {
|
|
||||||
background: #555;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.icon {
|
|
||||||
height: 1.5em;
|
|
||||||
width: 1.5em;
|
|
||||||
}
|
|
||||||
button.icon, a.icon {
|
|
||||||
display: flex;
|
|
||||||
align-content: baseline;
|
|
||||||
gap: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
main {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#search {
|
|
||||||
margin: 1em;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index-page {
|
|
||||||
margin: 1em auto;
|
|
||||||
align-content: center;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
.index-page section {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
.index-page section article {
|
|
||||||
margin: auto;
|
|
||||||
width: 300px;
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#results {
|
|
||||||
margin: 1em;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.clean {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.4rem;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #000;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#results a, section#results button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.4rem;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #000;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog {
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
top: 80px;
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
max-height: calc(100vh - 160px);
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
section.actions {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
article.single-holiday {
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
border-bottom: 2px solid #a0a0a0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.optional-selector {
|
|
||||||
border: 1px solid #666;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.optional-selector .header {
|
|
||||||
padding: 1em;
|
|
||||||
background: #666;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
color: white;
|
|
||||||
border-top-left-radius: 0.5em;
|
|
||||||
border-top-right-radius: 0.5em;
|
|
||||||
}
|
|
||||||
.optional-selector > *:not(.header) {
|
|
||||||
margin: 0 1em;
|
|
||||||
}
|
|
||||||
.hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.source-code {
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
.source-code, .source-code * {
|
|
||||||
background: #777;
|
|
||||||
color: #fff;
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
}
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
### v1.0.1
|
||||||
|
* Updated api documentation page
|
||||||
|
* Updated README.md
|
26
db.go
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,25 +16,22 @@ func envMustExist(env string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func envOrDefault(env string, defaultValue string) string {
|
||||||
|
if value, exists := os.LookupEnv(env); exists {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func connectToDb() (*sqlx.DB, error) {
|
func connectToDb() (*sqlx.DB, error) {
|
||||||
host := envMustExist("PSQL_HOST")
|
host := envMustExist("PSQL_HOST")
|
||||||
port := envMustExist("PSQL_PORT")
|
port := envMustExist("PSQL_PORT")
|
||||||
user := envMustExist("PSQL_USER")
|
user := envMustExist("PSQL_USER")
|
||||||
password := envMustExist("PSQL_PASSWORD")
|
password := envMustExist("PSQL_PASSWORD")
|
||||||
dbname := envMustExist("PSQL_DB")
|
dbname := envMustExist("PSQL_DB")
|
||||||
|
sslMode := envOrDefault("PSQL_SSLMODE", "disable")
|
||||||
|
schema := envOrDefault("PSQL_SCHEMA", "public")
|
||||||
|
|
||||||
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
return db.ConnectToDbNamed(host, port, user, password, dbname, sslMode, schema)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, sslMode string, schema string) (*sqlx.DB, error) {
|
||||||
|
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s search_path=%s",
|
||||||
|
host, port, user, password, dbname, sslMode, schema)
|
||||||
|
|
||||||
|
db, err := sqlx.Open("postgres", psqlInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS "country"
|
||||||
name varchar(45) NOT NULL,
|
name varchar(45) NOT NULL,
|
||||||
|
|
||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "holiday"
|
CREATE TABLE IF NOT EXISTS "holiday"
|
||||||
(
|
(
|
||||||
|
@ -19,21 +19,5 @@ CREATE TABLE IF NOT EXISTS "holiday"
|
||||||
|
|
||||||
primary key (id),
|
primary key (id),
|
||||||
constraint fk_country_id foreign key (country)
|
constraint fk_country_id foreign key (country)
|
||||||
references co
|
references country(iso_name) on delete cascade on update cascade
|
||||||
|
);
|
||||||
primary key (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "holiday"
|
|
||||||
(
|
|
||||||
id uuid,
|
|
||||||
country char(2) NOT NULL,
|
|
||||||
date date NOT NULL,
|
|
||||||
name varchar(64) NOT NULL,
|
|
||||||
description varchar(512),
|
|
||||||
is_state boolean NOT NULL,
|
|
||||||
is_religious boolean NOT NULL,
|
|
||||||
|
|
||||||
primary key (id),
|
|
||||||
constraint fk_country_id foreign key (cuntry(iso_name) on delete cascade on update cascade
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,38 +1,15 @@
|
||||||
package main
|
package holiday
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"holiday-api/holiday"
|
"strconv"
|
||||||
"net/http"
|
|
||||||
"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"})
|
|
||||||
} else {
|
|
||||||
render(c, http.StatusOK, mapHolidays(holidays))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DateResponse struct{ time.Time }
|
type DateResponse struct{ time.Time }
|
||||||
|
|
||||||
func (d DateResponse) MarshalJSON() ([]byte, error) {
|
func (d DateResponse) MarshalJSON() ([]byte, error) {
|
||||||
|
@ -54,6 +31,18 @@ type HolidayResponse struct {
|
||||||
Holidays []HolidayItemResponse `json:"holidays"`
|
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 {
|
type HolidayItemResponse struct {
|
||||||
XMLName xml.Name `json:"-" xml:"Holiday"`
|
XMLName xml.Name `json:"-" xml:"Holiday"`
|
||||||
Id uuid.UUID `json:"id" xml:"id,attr"`
|
Id uuid.UUID `json:"id" xml:"id,attr"`
|
||||||
|
@ -64,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{
|
|
@ -37,22 +37,22 @@ func (s *HolidayService) FindById(id uuid.UUID) (Holiday, error) {
|
||||||
|
|
||||||
func (s *HolidayService) findByDate(date time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) {
|
func (s *HolidayService) findByDate(date time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) {
|
||||||
var holidays []Holiday
|
var holidays []Holiday
|
||||||
return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE "date" = $1 AND country = $2 `+s.filter(isState, isReligious)+";", date, country)
|
return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE "date" = $1 AND country = $2 `+s.filter(isState, isReligious)+" ORDER BY \"date\";", date, country)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HolidayService) findForRange(rangeStart time.Time, rangeEnd time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) {
|
func (s *HolidayService) findForRange(rangeStart time.Time, rangeEnd time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) {
|
||||||
var holidays []Holiday
|
var holidays []Holiday
|
||||||
return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE "date" BETWEEN $1 AND $2 AND country = $3`+s.filter(isState, isReligious)+";", rangeStart, rangeEnd, country)
|
return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE "date" BETWEEN $1 AND $2 AND country = $3`+s.filter(isState, isReligious)+" ORDER BY \"date\";", rangeStart, rangeEnd, country)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HolidayService) findByYear(year int, isState *bool, isReligious *bool, country string) ([]Holiday, error) {
|
func (s *HolidayService) findByYear(year int, isState *bool, isReligious *bool, country string) ([]Holiday, error) {
|
||||||
var holidays []Holiday
|
var holidays []Holiday
|
||||||
return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE extract(year from "date") = $1 AND country = $2 `+s.filter(isState, isReligious)+";", year, country)
|
return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE extract(year from "date") = $1 AND country = $2 `+s.filter(isState, isReligious)+" ORDER BY \"date\";", year, country)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HolidayService) find(isState *bool, isReligious *bool, country string) ([]Holiday, error) {
|
func (s *HolidayService) find(isState *bool, isReligious *bool, country string) ([]Holiday, error) {
|
||||||
var holidays []Holiday
|
var holidays []Holiday
|
||||||
return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE country = $1 `+s.filter(isState, isReligious)+";", country)
|
return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE country = $1 `+s.filter(isState, isReligious)+" ORDER BY \"date\";", country)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HolidayService) paginate(holidays []Holiday, paging Paging) []Holiday {
|
func (s *HolidayService) paginate(holidays []Holiday, paging Paging) []Holiday {
|
||||||
|
@ -102,6 +102,23 @@ func (s *HolidayService) Delete(id uuid.UUID) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *HolidayService) Copy(country string, from int, to int) error {
|
||||||
|
holidays, err := s.findByYear(from, nil, nil, country)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var diff = to - from
|
||||||
|
|
||||||
|
for _, holiday := range holidays {
|
||||||
|
holiday.Date = holiday.Date.AddDate(diff, 0, 0)
|
||||||
|
_, err := s.Create(holiday)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getDateOrDefault(date *time.Time, year int) time.Time {
|
func getDateOrDefault(date *time.Time, year int) time.Time {
|
||||||
if date == nil {
|
if date == nil {
|
||||||
return time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
|
return time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
@ -34,6 +34,7 @@ type Search struct {
|
||||||
RangeEnd *time.Time `form:"range_end" time_format:"2006-01-02"`
|
RangeEnd *time.Time `form:"range_end" time_format:"2006-01-02"`
|
||||||
StateHoliday string `form:"state_holiday,omitempty" binding:"omitempty"`
|
StateHoliday string `form:"state_holiday,omitempty" binding:"omitempty"`
|
||||||
ReligiousHoliday string `form:"religious_holiday,omitempty" binding:"omitempty"`
|
ReligiousHoliday string `form:"religious_holiday,omitempty" binding:"omitempty"`
|
||||||
|
Type *string `form:"type,omitempty" binding:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Search) IsStateHoliday() *bool {
|
func (s Search) IsStateHoliday() *bool {
|
4
go.mod
|
@ -1,10 +1,10 @@
|
||||||
module holiday-api
|
module holiday-api
|
||||||
|
|
||||||
go 1.19
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jmoiron/sqlx v1.3.5
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
|
|
2
go.sum
|
@ -34,6 +34,8 @@ github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
|
331
main.go
|
@ -1,302 +1,89 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"github.com/gin-gonic/gin"
|
"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/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"runtime/debug"
|
||||||
"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") {
|
if !hasProfile("dev") {
|
||||||
isDev = true
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
if value := os.Getenv("LOG_FORMAT"); value == "json" {
|
||||||
|
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})))
|
||||||
}
|
}
|
||||||
log.SetPrefix("")
|
|
||||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
commit, time := buildInfo()
|
||||||
|
slog.Info("build info", slog.String("commit", commit), slog.String("time", time))
|
||||||
|
|
||||||
client, err := connectToDb()
|
client, err := connectToDb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("couldn't connect to db: %v", err)
|
slog.Error("couldn't connect to client", slog.String("err", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
migrationFolder := prodMigrations
|
if err := migration.InitializeMigrations(client, migrationFolder()); err != nil {
|
||||||
if isDev {
|
slog.Error("couldn't finish migration", slog.String("err", err.Error()))
|
||||||
migrationFolder = devMigrations
|
os.Exit(1)
|
||||||
}
|
|
||||||
if err := migration.InitializeMigrations(client, migrationFolder); err != nil {
|
|
||||||
log.Fatalf("couldn't execute migrations: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g := gin.Default()
|
server := api.SetupServer(client)
|
||||||
|
|
||||||
g.Static("assets", "assets")
|
port := ":" + getOrDefault("SERVER_PORT", "5281")
|
||||||
|
slog.Info("app is ready", slog.String("port", port))
|
||||||
loadTemplates(g)
|
if err := http.ListenAndServe(port, server); err != nil {
|
||||||
|
slog.Error("Couldn't start server!\n", slog.Any("err", err.Error()))
|
||||||
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) {
|
|
||||||
year := time.Now().Year()
|
|
||||||
search := holiday.Search{Country: "HR", Year: &year}
|
|
||||||
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})
|
|
||||||
})
|
|
||||||
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) {
|
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")
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrationFolder() fs.FS {
|
||||||
|
if hasProfile("dev") {
|
||||||
|
return db.DevMigrations
|
||||||
|
}
|
||||||
|
return db.ProdMigrations
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildInfo() (string, string) {
|
||||||
|
revision := ""
|
||||||
|
time := ""
|
||||||
|
|
||||||
|
if info, ok := debug.ReadBuildInfo(); ok {
|
||||||
|
for _, setting := range info.Settings {
|
||||||
|
if setting.Key == "vcs.revision" {
|
||||||
|
revision = setting.Value
|
||||||
|
} else if setting.Key == "vcs.time" {
|
||||||
|
time = setting.Value
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"deferint": func(value *int) int { return *value },
|
}
|
||||||
"intpeq": func(selected *int, value int) bool {
|
return revision, time
|
||||||
if selected != nil {
|
|
||||||
return *selected == value
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
g.LoadHTMLFiles(
|
|
||||||
"templates/index.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/edit-country.gohtml",
|
|
||||||
"templates/dialogs/delete-country.gohtml",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) {
|
func getOrDefault(env string, defaultValue string) string {
|
||||||
adminDashboard.Use(gin.BasicAuth(loadAuth()))
|
if value, present := os.LookupEnv(env); present {
|
||||||
|
return value
|
||||||
adminDashboard.GET("/", func(c *gin.Context) {
|
}
|
||||||
search := holiday.Search{Country: "HR", Year: new(int)}
|
return defaultValue
|
||||||
*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-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]}
|
|
||||||
}
|
}
|
||||||
|
|
20
makefile
|
@ -1,19 +1,11 @@
|
||||||
# scripts for building app
|
# scripts for building app
|
||||||
# requires go 1.19+ and git installed
|
# requires go 1.22+ and git installed
|
||||||
|
|
||||||
VERSION := 0.2.3
|
VERSION := $(shell git describe --tags --always)
|
||||||
|
|
||||||
serve:
|
|
||||||
go run ./...
|
|
||||||
|
|
||||||
setup:
|
|
||||||
go get
|
|
||||||
|
|
||||||
docker-dev:
|
docker-dev:
|
||||||
docker image build -t registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev .
|
docker image build -t registry.bbr-dev.info/holiday-api/backend/dev:latest .
|
||||||
docker tag registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev registry.bbr-dev.info/holiday-api/backend:latest-dev
|
docker image push registry.bbr-dev.info/holiday-api/backend/dev:latest
|
||||||
docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev
|
|
||||||
docker image push registry.bbr-dev.info/holiday-api/backend:latest-dev
|
|
||||||
|
|
||||||
|
|
||||||
docker-prod:
|
docker-prod:
|
||||||
|
@ -22,10 +14,6 @@ docker-prod:
|
||||||
docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION)
|
docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION)
|
||||||
docker image push registry.bbr-dev.info/holiday-api/backend:latest
|
docker image push registry.bbr-dev.info/holiday-api/backend:latest
|
||||||
|
|
||||||
release:
|
|
||||||
git tag $(VERSION)
|
|
||||||
git push origin $(VERSION)
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log/slog"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -92,7 +93,8 @@ func validateMigrations(db *sqlx.DB, migrations map[string]Migration, migrationF
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeMigration(db *sqlx.DB, name string, script string) error {
|
func executeMigration(db *sqlx.DB, name string, script string) error {
|
||||||
log.Printf("[INFO] script='%s' | migrations - executing", name)
|
logger := slog.Default().With(slog.String("script", name))
|
||||||
|
logger.Info("migrations - executing")
|
||||||
tx := db.MustBeginTx(context.Background(), nil)
|
tx := db.MustBeginTx(context.Background(), nil)
|
||||||
var err error = nil
|
var err error = nil
|
||||||
if _, e := tx.Exec(script); e != nil {
|
if _, e := tx.Exec(script); e != nil {
|
||||||
|
@ -102,10 +104,10 @@ func executeMigration(db *sqlx.DB, name string, script string) error {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] script='%s' | migrations - failed executing", name)
|
logger.Error("migrations - failed executing", slog.String("err", err.Error()))
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[INFO] script='%s' | migrations - succesfully executed", name)
|
logger.Info("migrations - successfully executed")
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
@ -119,9 +121,9 @@ func validateMigration(name string, migration Migration, script string) error {
|
||||||
calculatedHash := hash(script)
|
calculatedHash := hash(script)
|
||||||
|
|
||||||
if calculatedHash != migration.Hash {
|
if calculatedHash != migration.Hash {
|
||||||
err := fmt.Sprintf("migrations - mismatch in hash for %s (expected '%s', calculated '%s')", name, migration.Hash, calculatedHash)
|
err := errors.New(fmt.Sprintf("migrations - mismatch in hash for %s (expected '%s', calculated '%s')", name, migration.Hash, calculatedHash))
|
||||||
log.Printf("[ERROR] script='%s' err='%s' | migrations - failed executing", script, err)
|
slog.Error("migrations - failed validation", slog.String("script", name), slog.String("err", err.Error()))
|
||||||
return fmt.Errorf("migrations - mismatch in hashes for %s", name)
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
20
render.go
|
@ -1,20 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func render[T any](c *gin.Context, status int, response T) {
|
|
||||||
switch c.GetHeader("accept") {
|
|
||||||
case "application/xml":
|
|
||||||
fallthrough
|
|
||||||
case "text/xml":
|
|
||||||
c.Header("content-type", c.GetHeader("accept")+"; charset=utf-8")
|
|
||||||
c.XML(status, response)
|
|
||||||
case "application/json":
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
c.Header("content-type", "application/json; charset=utf-8")
|
|
||||||
c.JSON(status, response)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +1,98 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<title>Holiday-api | Admin</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta charset="utf-8">
|
||||||
<title>Holiday-api | Admin dashboard</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
|
||||||
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="/assets/global.css">
|
||||||
<script src="/assets/global.js"></script>
|
<script src="/assets/global.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="dialog-container"></div>
|
<div class="background-image" style="position: fixed">
|
||||||
|
</div>
|
||||||
<header>
|
<header>
|
||||||
<section class="container">
|
<h1><a href="/admin?country=HR">Holiday-api | Admin</a></h1>
|
||||||
<h1><a href="/">Holiday-api | Admin dashboard</a></h1>
|
<div style="flex-grow: 1"></div>
|
||||||
</section>
|
<a style="all: unset;" href="/admin/countries">Države</a>
|
||||||
</header>
|
</header>
|
||||||
<nav>
|
<main class="admin-dashboard">
|
||||||
<section class="container">
|
<div class="filter-dialog" style="background: transparent">
|
||||||
<a class="selected" href="#">Search</a>
|
<section class="dialog filter-dialog">
|
||||||
<a href="/admin/countries">Countries</a>
|
<h2 style="margin-top: 0.5em">Pretraga</h2>
|
||||||
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
|
|
||||||
</section>
|
|
||||||
</nav>
|
|
||||||
<main>
|
|
||||||
<section id="search">
|
|
||||||
<article class="card">
|
|
||||||
<form action="#" method="get">
|
<form action="#" method="get">
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="country">Country:</label>
|
<label for="country">Država:</label>
|
||||||
<select id="country" name="country">
|
<select id="country" name="country">
|
||||||
{{range $entry := .Countries}}
|
{{range $entry := .Countries}}
|
||||||
<option {{if eq $.Search.Country $entry.IsoName}}selected{{end}} value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
<option {{if eq $.Search.Country $entry.IsoName}}selected{{end}} value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="year">Year:</label>
|
<label for="year">Za godinu:</label>
|
||||||
<select id="year" name="year">
|
<select id="year" name="year">
|
||||||
{{range $entry := .Years}}
|
{{range $entry := .Years}}
|
||||||
<option {{if intpeq $.Search.Year $entry}}selected{{end}} value="{{$entry}}">{{$entry}}</option>
|
<option {{if intpeq $.Search.Year $entry}}selected{{end}} value="{{$entry}}">{{$entry}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
</section>
|
</section>
|
||||||
<section class="radio-group">
|
<section class="radio-group" style="margin-top: 0.5em;">
|
||||||
<label>Is state holiday:</label>
|
<label>Državni praznik:</label>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" value="true" name="sh" id="sh_true"><label for="sh_true">True
|
<input type="radio" value="true" name="sh" id="sh_true"><label for="sh_true">Da
|
||||||
</label><input type="radio" value="false" name="sh" id="sh_false"><label for="sh_false">False
|
</label><input type="radio" value="false" name="sh" id="sh_false"><label for="sh_false">Ne
|
||||||
</label><input type="radio" value="" name="sh" id="sh_any"><label for="sh_any">All</label>
|
</label><input type="radio" value="" name="sh" id="sh_any"><label for="sh_any">Svi</label>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" value="{{.Search.StateHoliday}}" name="state_holiday">
|
<input type="hidden" value="{{.Search.StateHoliday}}" id="state-holiday" name="state_holiday">
|
||||||
</section>
|
</section>
|
||||||
<section class="radio-group">
|
<section class="radio-group">
|
||||||
<label>Is religious holiday:</label>
|
<label>Religiozni praznik:</label>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" value="true" name="rh" id="rh_true"><label for="rh_true">True
|
<input type="radio" value="true" name="rh" id="rh_true"><label for="rh_true">Da
|
||||||
</label><input type="radio" value="false" name="rh" id="rh_false"><label for="rh_false">False
|
</label><input type="radio" value="false" name="rh" id="rh_false"><label for="rh_false">Ne
|
||||||
</label><input type="radio" value="" name="rh" id="rh_any"><label for="rh_any">All</label>
|
</label><input type="radio" value="" name="rh" id="rh_any"><label for="rh_any">Svi</label>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" value="{{.Search.ReligiousHoliday}}" name="religious_holiday">
|
<input type="hidden" value="{{.Search.ReligiousHoliday}}" id="religious-holiday" name="religious_holiday">
|
||||||
</section>
|
|
||||||
<section class="actions">
|
|
||||||
<button class="icon primary" type="submit">
|
|
||||||
<img class="icon" src="/assets/images/search.svg">
|
|
||||||
<span>Search</span>
|
|
||||||
</button>
|
|
||||||
</section>
|
</section>
|
||||||
|
<div class="button-actions">
|
||||||
|
<button class="primary">Pretraži</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
<section id="results">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>State</th>
|
|
||||||
<th>Religious</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
{{range $entry := .Holidays.Holidays}}
|
<div style="flex-grow: 1; background: transparent">
|
||||||
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: end; background: transparent; gap: 0.5em">
|
||||||
|
<button data-type="dialog" data-trigger="#copy-year" data-url="/admin/dialogs/copy-year?year={{$.Search.Year}}&country={{$.Search.Country}}" class="secondary">Kopiraj godinu</button>
|
||||||
|
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday" class="primary">Dodaj novi praznik</button>
|
||||||
|
</div>
|
||||||
|
<section class="dialog table-results" style="margin: 0; margin-top: 0.5em" id="results">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{$entry.Name}}</td>
|
<th>Ime</th>
|
||||||
<td>{{$entry.Date.Format "2006-01-02"}}</td>
|
<th>Datum</th>
|
||||||
<td><img class="icon" src="{{if $entry.IsStateHoliday}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
|
<th></th>
|
||||||
<td><img class="icon" src="{{if $entry.IsReligiousHoliday}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
|
|
||||||
<td>
|
|
||||||
<button data-type="dialog" data-trigger="#update-card" data-url="/admin/dialogs/edit-holiday?id={{$entry.Id}}" class="icon"><img class="icon" src="/assets/images/edit.svg"></button>
|
|
||||||
<button data-type="dialog" data-trigger="#delete-card" data-url="/admin/dialogs/delete-holiday?id={{$entry.Id}}" class="icon"><img class="icon" src="/assets/images/trash-delete.svg"></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
|
||||||
</section>
|
{{range $entry := .Holidays.Holidays}}
|
||||||
|
<tr>
|
||||||
|
<td>{{$entry.Name}}</td>
|
||||||
|
<td>{{$entry.Date.Format "02.01.2006"}}</td>
|
||||||
|
<td>
|
||||||
|
<button data-type="dialog" data-trigger="#update-card" data-url="/admin/dialogs/edit-holiday?id={{$entry.Id}}" class="icon"><span class="icon edit-icon"></span></button>
|
||||||
|
<button data-type="dialog" data-trigger="#delete-card" data-url="/admin/dialogs/delete-holiday?id={{$entry.Id}}" class="icon"><span class="icon delete-icon"></span></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,60 +1,50 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<title>Holiday-api | Države</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta charset="utf-8">
|
||||||
<title>Holiday-api | Admin dashboard</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
|
||||||
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="/assets/global.css">
|
||||||
<script src="/assets/global.js"></script>
|
<script src="/assets/global.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="dialog-container"></div>
|
<div class="background-image" style="position: fixed">
|
||||||
|
</div>
|
||||||
<header>
|
<header>
|
||||||
<section class="container">
|
<h1><a href="/admin?country=HR">Holiday-api | Države</a></h1>
|
||||||
<h1><a href="/">Holiday-api | Countries</a></h1>
|
|
||||||
</section>
|
|
||||||
</header>
|
</header>
|
||||||
<nav>
|
<main class="admin-dashboard">
|
||||||
<section class="container">
|
<div style="width: 640px; max-width: 100%; margin: auto; background: transparent">
|
||||||
<a href="/admin">Search</a>
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: end; background: transparent">
|
||||||
<a class="selected" href="#">Countries</a>
|
<button style="float: right; margin-top: 0" data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-country" class="primary">Dodaj državu</button>
|
||||||
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
|
</div>
|
||||||
</section>
|
<section class="dialog table-results" style="margin: 0; margin-top: 0.5em" id="results">
|
||||||
</nav>
|
|
||||||
<main>
|
|
||||||
|
|
||||||
<section style="margin: 1em; flex-grow: 1">
|
|
||||||
<form action="/admin/countries" method="post">
|
|
||||||
<table style="width: 100%">
|
<table style="width: 100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Iso name</th>
|
<th>Iso ime</th>
|
||||||
<th>Name</th>
|
<th>Ime</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td><section><input minlength="2" maxlength="2" required id="iso_name" type="text" name="iso_name"></section></td>
|
|
||||||
<td><section><input minlength="1" maxlength="45" required id="name" type="text" name="name"></section></td>
|
|
||||||
<td><button>Create country</button></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{{range $entry := .Countries}}
|
{{range $entry := .Countries}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{$entry.IsoName}}</td>
|
<td>{{$entry.IsoName}}</td>
|
||||||
<td>{{$entry.Name}}</td>
|
<td>{{$entry.Name}}</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" data-type="dialog" data-trigger="#update-card" data-url="/admin/dialogs/edit-country?id={{$entry.Id}}" class="clean icon"><img class="icon" src="/assets/images/edit.svg"></button>
|
<button type="button" data-type="dialog" data-trigger="#update-card" data-url="/admin/dialogs/edit-country?id={{$entry.Id}}" class="clean icon"><span class="icon edit-icon"></span></button>
|
||||||
<button type="button" data-type="dialog" data-trigger="#delete-card" data-url="/admin/dialogs/delete-country?id={{$entry.Id}}" class="clean icon"><img class="icon" src="/assets/images/trash-delete.svg"></button>
|
<button type="button" data-type="dialog" data-trigger="#delete-card" data-url="/admin/dialogs/delete-country?id={{$entry.Id}}" class="clean icon"><span class="icon delete-icon"></span></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<dialog class="card" id="create-card">
|
||||||
|
<h3 class="card-title">Dodaj državu</h3>
|
||||||
|
<form method="post" action="/admin/countries">
|
||||||
|
<section class="form-field">
|
||||||
|
<label for="name">Iso ime:</label>
|
||||||
|
<input required minlength="2" maxlength="2" id="iso_name" value="{{.Country.IsoName}}" name="iso_name" type="text">
|
||||||
|
</section>
|
||||||
|
<section class="form-field">
|
||||||
|
<label for="name">Ime:</label>
|
||||||
|
<input required minlength="1" maxlength="45" id="name" value="{{.Country.Name}}" name="name" type="text">
|
||||||
|
</section>
|
||||||
|
<section class="actions">
|
||||||
|
<button class="primary" type="submit">Dodaj</button>
|
||||||
|
<button class="secondary" type="button" onclick="closeDialog('#create-card')">Odustani</button>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
|
@ -1,35 +1,35 @@
|
||||||
<dialog class="card" id="create-card">
|
<dialog class="card" id="create-card">
|
||||||
<h3 class="card-title">Create holiday</h3>
|
<h3 class="card-title">Dodaj praznik</h3>
|
||||||
<form method="post" action="/admin/holidays">
|
<form method="post" action="/admin/holidays">
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="country">Country:</label>
|
<label for="country">Država:</label>
|
||||||
<select id="country" required name="country">
|
<select id="country" required name="country">
|
||||||
{{range $entry := .Countries}}
|
{{range $entry := .Countries}}
|
||||||
<option value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
<option value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="name">Name:</label>
|
<label for="name">Ime:</label>
|
||||||
<input required id="name" name="name" type="text">
|
<input required id="name" name="name" type="text">
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="description">Description:</label>
|
<label for="description">Opis:</label>
|
||||||
<textarea id="description" name="description"></textarea>
|
<textarea id="description" name="description"></textarea>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="date">Date:</label>
|
<label for="date">Datum:</label>
|
||||||
<input required id="date" name="date" type="date">
|
<input required id="date" name="date" type="date">
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label class="checkbox"><span>State holiday</span><input id="state_holiday" value="true" name="state_holiday" type="checkbox"></label>
|
<label class="checkbox"><span>Državni praznik</span><input id="state_holiday" value="true" name="state_holiday" type="checkbox"></label>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label class="checkbox"><span>Religious holiday</span><input id="religious_holiday" value="true" name="religious_holiday" type="checkbox"></label>
|
<label class="checkbox"><span>Religiozni praznik</span><input id="religious_holiday" value="true" name="religious_holiday" type="checkbox"></label>
|
||||||
</section>
|
</section>
|
||||||
<section class="actions">
|
<section class="actions">
|
||||||
<button type="submit">Create</button>
|
<button class="primary" type="submit">Dodaj</button>
|
||||||
<button type="button" onclick="closeDialog('#create-card')">Cancel</button>
|
<button class="secondary" type="button" onclick="closeDialog('#create-card')">Odustani</button>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
|
@ -1,23 +0,0 @@
|
||||||
<dialog class="card" data-closeable id="check-is-a-holiday">
|
|
||||||
<div style="display: flex;">
|
|
||||||
<h2 style="margin-right: 1em;">Is it a holiday?</h2>
|
|
||||||
<button onclick="closeDialog('#check-is-a-holiday')" class="clean icon"><img class="icon" src="/assets/images/close-x.svg"></button>
|
|
||||||
</div>
|
|
||||||
<form method="get" action="/search">
|
|
||||||
<section>
|
|
||||||
<label for="country">Country:</label>
|
|
||||||
<select id="country" name="country">
|
|
||||||
{{range $entry := .Countries}}
|
|
||||||
<option value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<label for="date">Date:</label>
|
|
||||||
<input id="date" name="date" type="date" required>
|
|
||||||
</section>
|
|
||||||
<section class="actions">
|
|
||||||
<button style="width: 100%;" type="submit">Check is a holiday</button>
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
<dialog class="card" id="copy-year">
|
||||||
|
<h3 class="card-title">Kopiraj godinu {{.Year}}</h3>
|
||||||
|
|
||||||
|
<p>Na koji godinu želite kopirati praznike iz godine {{.Year}}?</p>
|
||||||
|
<form method="post" action="/admin/holidays/copy">
|
||||||
|
<input type="hidden" name="from" value="{{.Year}}">
|
||||||
|
<input type="hidden" name="country" value="{{.Country}}">
|
||||||
|
<section class="form-field">
|
||||||
|
<label for="to">Godina:</label>
|
||||||
|
<input required id="to" name="to" type="number" step="1">
|
||||||
|
</section>
|
||||||
|
<section class="actions">
|
||||||
|
<button class="primary" type="submit">Kopiraj</button>
|
||||||
|
<button class="secondary" type="button" onclick="closeDialog('#copy-year')">Odustani</button>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
|
@ -1,11 +1,11 @@
|
||||||
|
|
||||||
<dialog class="card" id="delete-card">
|
<dialog class="card" id="delete-card">
|
||||||
<h3 class="card-title">Delete country</h3>
|
<h3 class="card-title">Obriši državu</h3>
|
||||||
<p>Are you sure you want to delete "{{.Country.Name}}"?<br>All holidays for given country will be deleted!</p>
|
<p>Jeste li sigurni da želite obrisati "{{.Country.Name}}"?<br>Svi uneseni praznici za danu državu će biti obrisani!</p>
|
||||||
<form method="post" action="/admin/countries/{{.Country.Id}}/delete">
|
<form method="post" action="/admin/countries/{{.Country.Id}}/delete">
|
||||||
<section class="actions">
|
<section class="actions">
|
||||||
<button type="submit">Delete</button>
|
<button class="primary" type="submit">Obriši</button>
|
||||||
<button type="button" onclick="closeDialog('#delete-card')">Cancel</button>
|
<button class="secondary" type="button" onclick="closeDialog('#delete-card')">Odustani</button>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
|
@ -1,11 +1,11 @@
|
||||||
|
|
||||||
<dialog class="card" id="delete-card">
|
<dialog class="card" id="delete-card">
|
||||||
<h3 class="card-title">Delete holiday</h3>
|
<h3 class="card-title">Ukloni praznik</h3>
|
||||||
<p>Are you sure you want to delete "{{.Holiday.Name}}"?</p>
|
<p>Jeste li sigurni da želite ukloniti praznik "{{.Holiday.Name}}"?</p>
|
||||||
<form method="post" action="/admin/holidays/{{.Holiday.Id}}/delete">
|
<form method="post" action="/admin/holidays/{{.Holiday.Id}}/delete">
|
||||||
<section class="actions">
|
<section class="actions">
|
||||||
<button type="submit">Delete</button>
|
<button class="primary" type="submit">Obriši</button>
|
||||||
<button type="button" onclick="closeDialog('#delete-card')">Cancel</button>
|
<button class="secondary" type="button" onclick="closeDialog('#delete-card')">Odustani</button>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
|
@ -1,18 +1,18 @@
|
||||||
<dialog class="card" id="update-card">
|
<dialog class="card" id="update-card">
|
||||||
<h3 class="card-title">Edit country</h3>
|
<h3 class="card-title">Ažuriraj državu</h3>
|
||||||
<form method="post" action="/admin/countries">
|
<form method="post" action="/admin/countries">
|
||||||
<input type="hidden" name="id" value="{{.Country.Id}}">
|
<input type="hidden" name="id" value="{{.Country.Id}}">
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="name">Iso name:</label>
|
<label for="name">Iso ime:</label>
|
||||||
<input required minlength="2" maxlength="2" id="iso_name" value="{{.Country.IsoName}}" name="iso_name" type="text">
|
<input required minlength="2" maxlength="2" id="iso_name" value="{{.Country.IsoName}}" name="iso_name" type="text">
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="name">Name:</label>
|
<label for="name">Ime:</label>
|
||||||
<input required minlength="1" maxlength="45" id="name" value="{{.Country.Name}}" name="name" type="text">
|
<input required minlength="1" maxlength="45" id="name" value="{{.Country.Name}}" name="name" type="text">
|
||||||
</section>
|
</section>
|
||||||
<section class="actions">
|
<section class="actions">
|
||||||
<button type="submit">Update</button>
|
<button class="primary" type="submit">Ažuriraj</button>
|
||||||
<button type="button" onclick="closeDialog('#update-card')">Cancel</button>
|
<button class="secondary" type="button" onclick="closeDialog('#update-card')">Odustani</button>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
|
@ -1,36 +1,36 @@
|
||||||
<dialog class="card" id="update-card">
|
<dialog class="card" id="update-card">
|
||||||
<h3 class="card-title">Edit holiday</h3>
|
<h3 class="card-title">Ažuriraj praznik</h3>
|
||||||
<form method="post" action="/admin/holidays">
|
<form method="post" action="/admin/holidays">
|
||||||
<input type="hidden" name="id" value="{{.Holiday.Id}}">
|
<input type="hidden" name="id" value="{{.Holiday.Id}}">
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="country">Country:</label>
|
<label for="country">Država:</label>
|
||||||
<select id="country" required name="country">
|
<select id="country" required name="country">
|
||||||
{{range $entry := .Countries}}
|
{{range $entry := .Countries}}
|
||||||
<option {{if eq $.Holiday.Country $entry.IsoName}}selected{{end}} value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
<option {{if eq $.Holiday.Country $entry.IsoName}}selected{{end}} value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="name">Name:</label>
|
<label for="name">Ime:</label>
|
||||||
<input required id="name" value="{{.Holiday.Name}}" name="name" type="text">
|
<input required id="name" value="{{.Holiday.Name}}" name="name" type="text">
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="description">Description:</label>
|
<label for="description">Opis:</label>
|
||||||
<textarea id="description" name="description">{{.Holiday.Description}}</textarea>
|
<textarea id="description" name="description">{{.Holiday.Description}}</textarea>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="date">Date:</label>
|
<label for="date">Datum:</label>
|
||||||
<input required id="date" value="{{.Holiday.Date.Format "2006-01-02"}}" name="date" type="date">
|
<input required id="date" value="{{.Holiday.Date.Format "2006-01-02"}}" name="date" type="date">
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label class="checkbox"><span>State holiday</span><input id="state_holiday" value="true" {{if .Holiday.IsStateHoliday}}checked{{end}} name="state_holiday" type="checkbox"></label>
|
<label class="checkbox"><span>Državni praznik</span><input id="state_holiday" value="true" {{if .Holiday.IsStateHoliday}}checked{{end}} name="state_holiday" type="checkbox"></label>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label class="checkbox"><span>Religious holiday</span><input id="religious_holiday" value="true" {{if .Holiday.IsReligiousHoliday}}checked{{end}} name="religious_holiday" type="checkbox"></label>
|
<label class="checkbox"><span>Religiozni praznik</span><input id="religious_holiday" value="true" {{if .Holiday.IsReligiousHoliday}}checked{{end}} name="religious_holiday" type="checkbox"></label>
|
||||||
</section>
|
</section>
|
||||||
<section class="actions">
|
<section class="actions">
|
||||||
<button type="submit">Update</button>
|
<button class="primary" type="submit">Ažuriraj</button>
|
||||||
<button type="button" onclick="closeDialog('#update-card')">Cancel</button>
|
<button class="secondary" type="button" onclick="closeDialog('#update-card')">Odustani</button>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
|
@ -1,127 +1,124 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="hr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<title>Holiday-api | Api dokumentacija</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta charset="utf-8">
|
||||||
<title>Holiday-api</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
|
||||||
|
<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">
|
||||||
<script src="/assets/global.js"></script>
|
<script src="/assets/global.js"></script>
|
||||||
<script src="/assets/documentation.js"></script>
|
<script src="/assets/documentation.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dropdown-content a {
|
||||||
|
all: unset;
|
||||||
|
display: block;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
}
|
||||||
|
.dropdown-content a:hover {
|
||||||
|
background: rgba(95, 158, 160, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="dialog-container"></div>
|
<div class="background-image" style="position: fixed;"></div>
|
||||||
<header>
|
<header>
|
||||||
<section class="container">
|
<h1><a href="/">Holiday-api</a> | Dokumentacija</h1>
|
||||||
<h1><a href="/">Holiday-api</a></h1>
|
|
||||||
</section>
|
|
||||||
</header>
|
</header>
|
||||||
<nav>
|
<main class="dialog documentation-dialog">
|
||||||
<section class="container">
|
<section style="margin-bottom: 1em;">
|
||||||
<a href="/">Search</a>
|
<section id="query">
|
||||||
<a class="selected" href="#">Documentation</a>
|
<article class="card" style="margin: 1em;">
|
||||||
<button data-type="dialog" data-trigger="#check-is-a-holiday" data-url="/dialogs/check-is-a-holiday">Check is a holiday</button>
|
<form id="query-generator">
|
||||||
</section>
|
<input type="hidden" id="country" name="country" value="HR">
|
||||||
</nav>
|
<section class="form-field">
|
||||||
<main>
|
<label for="content-type">Format</label>
|
||||||
<section id="query">
|
<select id="content-type" name="content-type">
|
||||||
<article class="card" style="margin: 1em;">
|
<option value="json">JSON</option>
|
||||||
<form id="query-generator">
|
<option value="xml">XML</option>
|
||||||
<section>
|
<option value="csv">CSV</option>
|
||||||
<label for="country">Country</label>
|
</select>
|
||||||
<select id="country" name="country">
|
</section>
|
||||||
{{range $entry := .Countries}}
|
|
||||||
<option value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
<article class="optional-selector">
|
||||||
<label for="content-type">Content type</label>
|
<div class="header">
|
||||||
<select id="content-type" name="content-type">
|
<section class="form-field">
|
||||||
<option value="application/json">JSON</option>
|
<label for="date-selector">Izbornik</label>
|
||||||
<option value="application/xml">XML</option>
|
<select data-type="section-selector" data-selector-prefix="date-selector" id="date-selector" name="date-selector">
|
||||||
<option value="text/csv">CSV</option>
|
<option value="year">Za godinu</option>
|
||||||
</select>
|
<option value="date">Za datum</option>
|
||||||
</section>
|
<option value="range">Za raspon datuma</option>
|
||||||
|
<option value="all">Svi</option>
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div data-section-id="date-selector-year">
|
||||||
|
<section class="form-field">
|
||||||
|
<label for="dsy-year">Godina</label>
|
||||||
|
<select id="dsy-year" name="year">
|
||||||
|
{{range $entry := .Years}}
|
||||||
|
<option {{if eq $.Year $entry}}selected{{end}} value="{{$entry}}">{{$entry}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<article class="optional-selector">
|
<div data-section-id="date-selector-date">
|
||||||
<div class="header">
|
<section class="form-field">
|
||||||
<section>
|
<label for="dsd-date">Datum</label>
|
||||||
<label for="date-selector">Date selector</label>
|
<input type="date" id="dsd-date" name="dsd-date">
|
||||||
<select data-type="section-selector" data-selector-prefix="date-selector" id="date-selector" name="date-selector">
|
</section>
|
||||||
<option value="year">Select year</option>
|
</div>
|
||||||
<option value="date">Select date</option>
|
|
||||||
<option value="range">Select date range</option>
|
|
||||||
<option value="all">All</option>
|
|
||||||
</select>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-section-id="date-selector-year">
|
<div class="range-selector" data-section-id="date-selector-range">
|
||||||
<section>
|
<section class="form-field">
|
||||||
<label for="dsy-year">Year</label>
|
<label for="dsr-start-range">Početak raspona</label>
|
||||||
<select id="dsy-year" name="year">
|
<input type="date" id="dsr-start-range" name="dsr-start-range">
|
||||||
{{range $entry := .Years}}
|
</section>
|
||||||
<option value="{{$entry}}">{{$entry}}</option>
|
<section class="form-field">
|
||||||
{{end}}
|
<label for="dsr-end-range">Kraj raspona</label>
|
||||||
</select>
|
<input type="date" id="dsr-end-range" name="dsr-end-range">
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
<div data-section-id="date-selector-date">
|
<section class="radio-group" style="margin-top: 0.5em;">
|
||||||
<section>
|
<label>Državni praznik:</label>
|
||||||
<label for="dsd-date">Date</label>
|
<div>
|
||||||
<input type="date" id="dsd-date" name="dsd-date">
|
<input type="radio" value="true" name="sh" id="sh_true"><label for="sh_true">Da
|
||||||
</section>
|
</label><input type="radio" value="false" name="sh" id="sh_false"><label for="sh_false">Ne
|
||||||
</div>
|
</label><input type="radio" value="" name="sh" id="sh_any"><label for="sh_any">Svi</label>
|
||||||
|
</div>
|
||||||
<div data-section-id="date-selector-range">
|
<input type="hidden" id="state-holiday" name="state_holiday">
|
||||||
<section>
|
</section>
|
||||||
<label for="dsr-start-range">Start range</label>
|
<section class="radio-group">
|
||||||
<input type="date" id="dsr-start-range" name="dsr-start-range">
|
<label>Religiozni praznik:</label>
|
||||||
</section>
|
<div>
|
||||||
<section>
|
<input type="radio" value="true" name="rh" id="rh_true"><label for="rh_true">Da
|
||||||
<label for="dsr-start-range-required">Required</label>
|
</label><input type="radio" value="false" name="rh" id="rh_false"><label for="rh_false">Ne
|
||||||
<input type="checkbox" id="dsr-start-range-required" name="dsr-start-range-required">
|
</label><input type="radio" value="" name="rh" id="rh_any"><label for="rh_any">Svi</label>
|
||||||
</section>
|
</div>
|
||||||
<section>
|
<input type="hidden" id="religious-holiday" name="religious_holiday">
|
||||||
<label for="dsr-end-range">End range</label>
|
</section>
|
||||||
<input type="date" id="dsr-end-range" name="dsr-end-range">
|
</form>
|
||||||
</section>
|
</article>
|
||||||
<section>
|
</section>
|
||||||
<label for="dsr-end-range-required">Required</label>
|
<section id="result" style="flex-grow: 1; margin: 1em;">
|
||||||
<input type="checkbox" id="dsr-end-range-required" name="dsr-end-range-required">
|
<div style="display: flex; flex-wrap: wrap; align-items: baseline">
|
||||||
</section>
|
<h3 style="margin-bottom: 1em;">Naredba</h3>
|
||||||
</div>
|
<div style="flex-grow: 1"></div>
|
||||||
</article>
|
<div>
|
||||||
|
<a id="query-link" style="text-decoration: none" class="button secondary" href="." target="_blank">Dohvati</a>
|
||||||
<section class="radio-group" style="margin-top: 0.5em;">
|
</div>
|
||||||
<label>Is state holiday:</label>
|
</div>
|
||||||
<div>
|
<article id="result-content">
|
||||||
<input type="radio" value="true" name="sh" id="sh_true"><label for="sh_true">True
|
Učitavanje...
|
||||||
</label><input type="radio" value="false" name="sh" id="sh_false"><label for="sh_false">False
|
</article>
|
||||||
</label><input type="radio" value="" name="sh" id="sh_any"><label for="sh_any">All</label>
|
</section>
|
||||||
</div>
|
|
||||||
<input type="hidden" id="state-holiday" name="state_holiday">
|
|
||||||
</section>
|
|
||||||
<section class="radio-group">
|
|
||||||
<label>Is religious holiday:</label>
|
|
||||||
<div>
|
|
||||||
<input type="radio" value="true" name="rh" id="rh_true"><label for="rh_true">True
|
|
||||||
</label><input type="radio" value="false" name="rh" id="rh_false"><label for="rh_false">False
|
|
||||||
</label><input type="radio" value="" name="rh" id="rh_any"><label for="rh_any">All</label>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" id="religious-holiday" name="religious_holiday">
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
<section id="result" style="flex-grow: 1; margin: 1em;">
|
|
||||||
<h3 style="margin-bottom: 1em;">Query</h3>
|
|
||||||
<article id="result-content" class="card source-code">
|
|
||||||
Loading...
|
|
||||||
</article>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,95 +1,38 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="hr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<title>Holiday-api | Pronađi praznike</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta charset="utf-8">
|
||||||
<title>Holiday-api</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
|
||||||
|
<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">
|
||||||
<script src="/assets/global.js"></script>
|
<script src="/assets/global.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="dialog-container"></div>
|
<div class="background-image">
|
||||||
<header>
|
<header>
|
||||||
<section class="container">
|
|
||||||
<h1><a href="/">Holiday-api</a></h1>
|
<h1><a href="/">Holiday-api</a></h1>
|
||||||
</section>
|
<div style="flex-grow: 1"></div>
|
||||||
</header>
|
<a style="all: unset;" href="/documentation">Api dokumentacija</a>
|
||||||
<nav>
|
</header>
|
||||||
<section class="container">
|
<main class="dialog search-dialog">
|
||||||
<a class="selected" href="#">Search</a>
|
<form action="/">
|
||||||
<a href="/documentation">Documentation</a>
|
<div class="form-field">
|
||||||
<button data-type="dialog" data-trigger="#check-is-a-holiday" data-url="/dialogs/check-is-a-holiday">Check is a holiday</button>
|
<label for="year">Za godinu:</label>
|
||||||
</section>
|
<select id="year" name="year">
|
||||||
</nav>
|
{{range $entry := .Years}}
|
||||||
<main>
|
<option {{if eq $.Year $entry}}selected{{end}} value="{{$entry}}">{{$entry}}</option>
|
||||||
<section id="search">
|
{{end}}
|
||||||
<article class="card">
|
</select>
|
||||||
<form action="/" method="get">
|
</div>
|
||||||
<section>
|
<div class="button-actions">
|
||||||
<label for="country">Country:</label>
|
<button class="primary">Pretraži</button>
|
||||||
<select id="country" name="country">
|
</div>
|
||||||
{{range $entry := .Countries}}
|
</form>
|
||||||
<option {{if eq $.Search.Country $entry.IsoName}}selected{{end}} value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
</main>
|
||||||
{{end}}
|
</div>
|
||||||
</select>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<label for="year">Year:</label>
|
|
||||||
<select id="year" name="year">
|
|
||||||
{{range $entry := .Years}}
|
|
||||||
<option {{if intpeq $.Search.Year $entry}}selected{{end}} value="{{$entry}}">{{$entry}}</option>
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
</section>
|
|
||||||
<section class="radio-group">
|
|
||||||
<label>Is state holiday:</label>
|
|
||||||
<div>
|
|
||||||
<input type="radio" value="true" name="sh" id="sh_true"><label for="sh_true">True
|
|
||||||
</label><input type="radio" value="false" name="sh" id="sh_false"><label for="sh_false">False
|
|
||||||
</label><input type="radio" value="" name="sh" id="sh_any"><label for="sh_any">All</label>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" value="{{.Search.StateHoliday}}" name="state_holiday">
|
|
||||||
</section>
|
|
||||||
<section class="radio-group">
|
|
||||||
<label>Is religious holiday:</label>
|
|
||||||
<div>
|
|
||||||
<input type="radio" value="true" name="rh" id="rh_true"><label for="rh_true">True
|
|
||||||
</label><input type="radio" value="false" name="rh" id="rh_false"><label for="rh_false">False
|
|
||||||
</label><input type="radio" value="" name="rh" id="rh_any"><label for="rh_any">All</label>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" value="{{.Search.ReligiousHoliday}}" name="religious_holiday">
|
|
||||||
</section>
|
|
||||||
<section class="actions">
|
|
||||||
<button class="icon primary" type="submit">
|
|
||||||
<img class="icon" src="/assets/images/search.svg">
|
|
||||||
<span>Search</span>
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
<section id="results">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>State</th>
|
|
||||||
<th>Religious</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{range $entry := .Holidays}}
|
|
||||||
<tr>
|
|
||||||
<td>{{$entry.Name}}</td>
|
|
||||||
<td>{{$entry.Date.Format "2006-01-02"}}</td>
|
|
||||||
<td><img class="icon" src="{{if $entry.IsStateHoliday}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
|
|
||||||
<td><img class="icon" src="{{if $entry.IsReligiousHoliday}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,94 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="hr">
|
||||||
|
<head>
|
||||||
|
<title>Holiday-api | Praznici za {{deferint $.Search.Year}}</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<script src="/assets/global.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--dropdown-background);
|
||||||
|
min-width: 160px;
|
||||||
|
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content.selected {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.dropdown-content a {
|
||||||
|
all: unset;
|
||||||
|
display: block;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
}
|
||||||
|
.dropdown-content a:hover {
|
||||||
|
background: rgba(95, 158, 160, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="background-image" style="position: fixed;"></div>
|
||||||
|
<header>
|
||||||
|
<h1><a href="/">Holiday-api</a> | {{deferint $.Search.Year}}</h1>
|
||||||
|
<div style="flex-grow: 1"></div>
|
||||||
|
<a style="all: unset;" href="/documentation">Api dokumentacija</a>
|
||||||
|
</header>
|
||||||
|
<main class="dialog results-dialog">
|
||||||
|
<section style="margin-bottom: 1em;">
|
||||||
|
<form style="display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 1em" action="/">
|
||||||
|
<div style="flex-grow: 1;"></div>
|
||||||
|
<div class="form-field" style="width: auto;" >
|
||||||
|
<label for="year">Za godinu:</label>
|
||||||
|
<select id="year" name="year" style="width: 150px">
|
||||||
|
{{range $entry := .Years}}
|
||||||
|
<option {{if intpeq $.Search.Year $entry}}selected{{end}} value="{{$entry}}">{{$entry}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="button-actions">
|
||||||
|
<button class="primary">Pretraži</button>
|
||||||
|
</div>
|
||||||
|
<div class="button-actions" style="margin-left: auto">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button type="button" class="secondary dropdown-action">Preuzmi</button>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a target="_blank" href="/api/v1/holidays?country=HR&type=csv&year={{deferint $.Search.Year}}">Kao csv</a>
|
||||||
|
<a target="_blank" href="/api/v1/holidays?country=HR&type=json&year={{deferint $.Search.Year}}">Kao json</a>
|
||||||
|
<a target="_blank" href="/api/v1/holidays?country=HR&type=xml&year={{deferint $.Search.Year}}">Kao xml</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<table class="results">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Ime</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $entry := .Holidays}}
|
||||||
|
<tr>
|
||||||
|
<td>{{$entry.Name}}</td>
|
||||||
|
<td>{{$entry.Date.Format "02.01.2006."}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,76 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Holiday-api | Admin dashboard</title>
|
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
|
||||||
<script src="/assets/global.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="dialog-container"></div>
|
|
||||||
<header>
|
|
||||||
<section class="container">
|
|
||||||
<h1><a href="/">Holiday-api | {{.Search.Date.Format "2006-01-02"}}</a></h1>
|
|
||||||
</section>
|
|
||||||
</header>
|
|
||||||
<nav>
|
|
||||||
<section class="container">
|
|
||||||
<a href="/">Search</a>
|
|
||||||
<a href="/documentation">Documentation</a>
|
|
||||||
<a class="selected" href="#">For date</a>
|
|
||||||
</section>
|
|
||||||
</nav>
|
|
||||||
<main>
|
|
||||||
<section id="search">
|
|
||||||
<article class="card">
|
|
||||||
<h2 style="margin-right: 1em;">Is it a holiday?</h2>
|
|
||||||
<form method="get" action="/search">
|
|
||||||
<section>
|
|
||||||
<label for="country">Country:</label>
|
|
||||||
<select id="country" name="country">
|
|
||||||
{{range $entry := .Countries}}
|
|
||||||
<option {{if eq $.Search.Country $entry.IsoName}}selected{{end}} value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<label for="date">Date:</label>
|
|
||||||
<input id="date" value="{{.Search.Date.Format "2006-01-02"}}" name="date" type="date" required>
|
|
||||||
</section>
|
|
||||||
<section class="actions">
|
|
||||||
<button style="width: 100%;" type="submit">Check is a holiday</button>
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
<section id="results">
|
|
||||||
<h2 style="margin-bottom: 1em;">Results</h2>
|
|
||||||
{{range $entry := .Holidays}}
|
|
||||||
<article class="single-holiday">
|
|
||||||
<table class="clean">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>Name: </th>
|
|
||||||
<td>{{$entry.Name}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Description: </th>
|
|
||||||
<td>{{$entry.Description}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Is state holiday: </th>
|
|
||||||
<td><img class="icon" src="{{if $entry.IsStateHoliday}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Is religious holiday: </th>
|
|
||||||
<td><img class="icon" src="{{if $entry.IsReligiousHoliday}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</article>
|
|
||||||
{{end}}
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|