Compare commits

..

2 Commits

Author SHA1 Message Date
Borna Rajković 45b220c69f v0.5.0
added year duplication
2024-02-08 21:30:20 +01:00
Borna Rajković 8fbdffc965 v0.4.0
fixed sorting and added dark theme
2024-02-08 20:48:23 +01:00
9 changed files with 216 additions and 61 deletions

BIN
assets/background-dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

View File

@ -4,8 +4,62 @@ body {
padding: 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 { main {
background: #f3ebe6; background: var(--background-color);
width: 640px; width: 640px;
max-width: 100%; max-width: 100%;
position: relative; position: relative;
@ -15,11 +69,11 @@ header {
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 1em; padding: 1em;
background: white; background: var(--background);
position: sticky; position: sticky;
z-index: 10; z-index: 10;
top: 0; top: 0;
border-bottom: 1px solid #dddddd; border-bottom: 1px solid var(--border);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
@ -29,8 +83,8 @@ header {
.dialog { .dialog {
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #e0e0e0; border: 1px solid var(--border);
background: white; background: var(--background);
margin: auto; margin: auto;
padding: 1em 0.5em; padding: 1em 0.5em;
} }
@ -55,6 +109,21 @@ header {
background-size: cover; 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-face {
font-family: Quicksand; font-family: Quicksand;
src: url(fonts/Quicksand-Light.ttf); src: url(fonts/Quicksand-Light.ttf);
@ -108,13 +177,13 @@ h3 {
} }
.form-field label { .form-field label {
display: block; display: block;
color: #999; color: var(--form-label);
} }
.form-field input, .form-field textarea { .form-field input, .form-field textarea {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
border: none; border: none;
border-bottom: 1px solid #bbb; border-bottom: 1px solid var(--form-border);
font-size: 1rem; font-size: 1rem;
outline: none; outline: none;
@ -127,47 +196,57 @@ h3 {
.form-field input[type=checkbox] { .form-field input[type=checkbox] {
width: inherit; width: inherit;
margin-left: 1em; margin-left: 1em;
accent-color: #6a7579; accent-color: var(--primary-accent);
} }
.form-field input:focus, .form-field textarea:focus { .form-field input:focus, .form-field textarea:focus {
color: cadetblue; color: var(--primary);
border-bottom: 1px solid cadetblue; border-bottom: 1px solid var(--primary);
} }
.form-field input:not(focus), .form-field textarea:not(focus) { .form-field input:not(focus), .form-field textarea:not(focus) {
color: #333; color: var(--form-focus);
} }
.form-field select { .form-field select {
all: unset; all: unset;
width: 100%; width: 100%;
border: none; border: none;
border-bottom: 1px solid #bbb; border-bottom: 1px solid var(--form-border);
font-size: 1rem; font-size: 1rem;
outline: none; outline: none;
} }
.form-field select:focus { .form-field select:focus {
color: cadetblue; color: var(--primary);
border-bottom: 1px solid cadetblue; border-bottom: 1px solid var(--primary);
} }
.form-field select:not(focus) { .form-field select:not(focus) {
color: #333; color: var(--form-focus);
} }
.form-field:focus-within label { .form-field:focus-within label {
color: cadetblue; color: var(--primary);
} }
.button.primary, button.primary { .button.primary, button.primary {
padding: 0.75em 1em; padding: 0.75em 1em;
background-color: cadetblue; background-color: var(--primary);
font-family: Quicksand, Arial, sans-serif; font-family: Quicksand, Arial, sans-serif;
border: none; border: none;
color: white; 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; font-size: 1rem;
margin-top: 1em; margin-top: 1em;
} }
@ -175,19 +254,17 @@ h3 {
.button.primary, button.primary:hover { .button.primary, button.primary:hover {
filter: contrast(140%); filter: contrast(140%);
} }
.button.secondary:hover, button.secondary:hover {
.button.secondary, button.secondary { filter: contrast(80%);
padding: 0.75em 1em;
background-color: white;
font-family: Quicksand, Arial, sans-serif;
border: 1px solid cadetblue;
color: cadetblue;
font-size: 1rem;
margin-top: 1em;
} }
@media (prefers-color-scheme: dark) {
.button.primary, button.primary:hover {
filter: brightness(140%);
}
.button.secondary:hover, button.secondary:hover { .button.secondary:hover, button.secondary:hover {
filter: contrast(140%); filter: brightness(140%);
}
} }
/*index.html*/ /*index.html*/
@ -197,7 +274,6 @@ h3 {
width: 100%; width: 100%;
padding-top: 40px; padding-top: 40px;
padding-bottom: 40px; padding-bottom: 40px;
border: none;
} }
@media screen and (min-width: 480px) { @media screen and (min-width: 480px) {
@ -218,7 +294,6 @@ h3 {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
padding-bottom: 40px; padding-bottom: 40px;
border: none;
} }
.results-dialog table { .results-dialog table {
@ -249,7 +324,6 @@ h3 {
position: relative; position: relative;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
border: none;
} }
@media screen and (min-width: 480px) { @media screen and (min-width: 480px) {
@ -263,7 +337,8 @@ h3 {
.documentation-dialog #result-content { .documentation-dialog #result-content {
font-family: 'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;
background: #aaa; background: var(--code-background);
color: var(--code-color);
padding: 1em; padding: 1em;
word-break: break-word; word-break: break-word;
} }
@ -280,9 +355,9 @@ section.radio-group div label {
display: inline-block; display: inline-block;
padding: 0.75em 1em; padding: 0.75em 1em;
border: none; border: none;
color: black; color: var(--color);
font-size: 1rem; font-size: 1rem;
background: #ddd; background: var(--border);
} }
section.radio-group div label { section.radio-group div label {
@ -303,8 +378,8 @@ section.radio-group div label:last-of-type {
} }
section.radio-group div input:checked+label { section.radio-group div input:checked+label {
background: cadetblue; background: var(--primary);
color: white; color: var(--primary-contrast);
} }
.documentation-dialog .range-selector { .documentation-dialog .range-selector {
@ -362,7 +437,18 @@ section.radio-group div input:checked+label {
} }
} }
img.icon, button.icon svg { .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; height: 1.2em;
width: 1.2em; width: 1.2em;
} }
@ -378,16 +464,16 @@ button.icon, a.icon {
} }
button.icon svg, button.icon svg * { button.icon svg, button.icon svg * {
fill: white; fill: var(--primary-contrast);
color: white; color: var(--primary-contrast);
stroke: white; stroke: var(--primary-contrast);
} }
dialog { dialog {
border: 1px solid rgba(95, 158, 160, 0.5); border: 1px solid var(--dialog-border);
} }
dialog::backdrop { dialog::backdrop {
background: rgba(151, 159, 159, 0.39); background: var(--dialog-backdrop);
} }
dialog { dialog {
width: 400px; width: 400px;

View File

@ -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)

29
main.go
View File

@ -110,6 +110,8 @@ func loadTemplates(g *gin.Engine) {
"templates/admin_dashboard.gohtml", "templates/admin_dashboard.gohtml",
"templates/countries.gohtml", "templates/countries.gohtml",
"templates/dialogs/copy-year.gohtml",
"templates/dialogs/add-holiday.gohtml", "templates/dialogs/add-holiday.gohtml",
"templates/dialogs/edit-holiday.gohtml", "templates/dialogs/edit-holiday.gohtml",
"templates/dialogs/delete-holiday.gohtml", "templates/dialogs/delete-holiday.gohtml",
@ -189,6 +191,24 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida
c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.Date.Year()), 10)) c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.Date.Year()), 10))
} }
}) })
adminDashboard.POST("/holidays/copy", func(c *gin.Context) {
request := struct {
From int `form:"from"`
To int `form:"to"`
Country string `form:"country" binding:"len=2"`
}{}
if err := c.ShouldBind(&request); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
err := service.Copy(request.Country, request.From, request.To)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
} else {
c.Redirect(http.StatusSeeOther, "/admin?country="+request.Country+"&year="+strconv.FormatInt(int64(request.To), 10))
}
})
adminDashboard.POST("/holidays/:id/delete", func(c *gin.Context) { adminDashboard.POST("/holidays/:id/delete", func(c *gin.Context) {
id := uuid.MustParse(c.Param("id")) id := uuid.MustParse(c.Param("id"))
hol, err := service.FindById(id) hol, err := service.FindById(id)
@ -266,6 +286,15 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida
countries, _ := countryService.Find() countries, _ := countryService.Find()
c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol}) c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol})
}) })
adminDashboard.GET("/dialogs/copy-year", func(c *gin.Context) {
country := c.Query("country")
year, err := strconv.ParseInt(c.Query("year"), 10, 32)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
c.HTML(http.StatusOK, "copy-year.gohtml", gin.H{"Country": country, "Year": year})
})
adminDashboard.GET("/dialogs/edit-country", func(c *gin.Context) { adminDashboard.GET("/dialogs/edit-country", func(c *gin.Context) {
id := uuid.MustParse(c.Query("id")) id := uuid.MustParse(c.Query("id"))
country, err := countryService.FindById(id) country, err := countryService.FindById(id)

View File

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

View File

@ -18,7 +18,7 @@
<a style="all: unset;" href="/admin/countries">Države</a> <a style="all: unset;" href="/admin/countries">Države</a>
</header> </header>
<main class="admin-dashboard"> <main class="admin-dashboard">
<div class="filter-dialog"> <div class="filter-dialog" style="background: transparent">
<section class="dialog filter-dialog"> <section class="dialog filter-dialog">
<h2 style="margin-top: 0.5em">Pretraga</h2> <h2 style="margin-top: 0.5em">Pretraga</h2>
<form action="#" method="get"> <form action="#" method="get">
@ -63,9 +63,12 @@
</section> </section>
</div> </div>
<div style="flex-grow: 1"> <div style="flex-grow: 1; background: transparent">
<button style="float: right; margin-top: 0" data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday" class="primary">Dodaj novi praznik</button> <div style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: end; background: transparent; gap: 0.5em">
<section class="dialog table-results" style="margin: 0; margin-top: 3.5em" id="results"> <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> <table>
<thead> <thead>
<tr> <tr>
@ -81,8 +84,8 @@
<td>{{$entry.Name}}</td> <td>{{$entry.Name}}</td>
<td>{{$entry.Date.Format "02.01.2006"}}</td> <td>{{$entry.Date.Format "02.01.2006"}}</td>
<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/icons/edit.svg"></button> <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"><img class="icon" src="/assets/icons/trash-delete.svg"></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> </td>
</tr> </tr>
{{end}} {{end}}

View File

@ -16,9 +16,11 @@
<h1><a href="/admin?country=HR">Holiday-api | Države</a></h1> <h1><a href="/admin?country=HR">Holiday-api | Države</a></h1>
</header> </header>
<main class="admin-dashboard"> <main class="admin-dashboard">
<div style="width: 640px; max-width: 100%; margin: auto"> <div style="width: 640px; max-width: 100%; margin: auto; background: transparent">
<div style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: end; background: transparent">
<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 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>
<section class="dialog table-results" style="margin: 0; margin-top: 3.5em" id="results"> </div>
<section class="dialog table-results" style="margin: 0; margin-top: 0.5em" id="results">
<table style="width: 100%"> <table style="width: 100%">
<thead> <thead>
<tr> <tr>
@ -34,8 +36,8 @@
<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/icons/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/icons/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}}

View File

@ -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>

View File

@ -18,9 +18,9 @@
.dropdown-content { .dropdown-content {
display: none; display: none;
position: absolute; position: absolute;
background-color: #f9f9f9; background-color: var(--dropdown-background);
min-width: 160px; min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
z-index: 1; z-index: 1;
} }