@@ -18,9 +18,16 @@ import ( | |||||
type CalendarRenderData struct { | type CalendarRenderData struct { | ||||
alps.BaseRenderData | alps.BaseRenderData | ||||
Time time.Time | Time time.Time | ||||
Now time.Time | |||||
Dates [7 * 6]time.Time | |||||
Calendar *caldav.Calendar | Calendar *caldav.Calendar | ||||
Events []CalendarObject | Events []CalendarObject | ||||
PrevPage, NextPage string | PrevPage, NextPage string | ||||
PrevTime, NextTime time.Time | |||||
EventsForDate func(time.Time) []CalendarObject | |||||
DaySuffix func(n int) string | |||||
Sub func(a, b int) int | |||||
} | } | ||||
type EventRenderData struct { | type EventRenderData struct { | ||||
@@ -96,13 +103,66 @@ func registerRoutes(p *alps.GoPlugin, u *url.URL) { | |||||
return fmt.Errorf("failed to query calendar: %v", err) | return fmt.Errorf("failed to query calendar: %v", err) | ||||
} | } | ||||
// TODO: Time zones are hard | |||||
var dates [7 * 6]time.Time | |||||
initialDate := start.UTC() | |||||
initialDate = initialDate.AddDate(0, 0, -int(initialDate.Weekday())) | |||||
for i := 0; i < len(dates); i += 1 { | |||||
dates[i] = initialDate | |||||
initialDate = initialDate.AddDate(0, 0, 1) | |||||
} | |||||
eventMap := make(map[time.Time][]CalendarObject) | |||||
for _, ev := range events { | |||||
ev := ev // make a copy | |||||
// TODO: include event on each date for which it is active | |||||
co := ev.Data.Events()[0] | |||||
startTime, _ := co.DateTimeStart(nil) | |||||
startTime = startTime.UTC().Truncate(time.Hour * 24) | |||||
eventMap[startTime] = append(eventMap[startTime], CalendarObject{&ev}) | |||||
} | |||||
return ctx.Render(http.StatusOK, "calendar.html", &CalendarRenderData{ | return ctx.Render(http.StatusOK, "calendar.html", &CalendarRenderData{ | ||||
BaseRenderData: *alps.NewBaseRenderData(ctx), | BaseRenderData: *alps.NewBaseRenderData(ctx), | ||||
Time: start, | Time: start, | ||||
Now: time.Now(), // TODO: Use client time zone | |||||
Calendar: calendar, | Calendar: calendar, | ||||
Dates: dates, | |||||
Events: newCalendarObjectList(events), | Events: newCalendarObjectList(events), | ||||
PrevPage: start.AddDate(0, -1, 0).Format(monthPageLayout), | PrevPage: start.AddDate(0, -1, 0).Format(monthPageLayout), | ||||
NextPage: start.AddDate(0, 1, 0).Format(monthPageLayout), | NextPage: start.AddDate(0, 1, 0).Format(monthPageLayout), | ||||
PrevTime: start.AddDate(0, -1, 0), | |||||
NextTime: start.AddDate(0, 1, 0), | |||||
EventsForDate: func(when time.Time) []CalendarObject { | |||||
if events, ok := eventMap[when.Truncate(time.Hour * 24)]; ok { | |||||
return events | |||||
} | |||||
return nil | |||||
}, | |||||
DaySuffix: func(n int) string { | |||||
if n % 100 >= 11 && n % 100 <= 13 { | |||||
return "th" | |||||
} | |||||
return map[int]string{ | |||||
0: "th", | |||||
1: "st", | |||||
2: "nd", | |||||
3: "rd", | |||||
4: "th", | |||||
5: "th", | |||||
6: "th", | |||||
7: "th", | |||||
8: "th", | |||||
9: "th", | |||||
}[n % 10] | |||||
}, | |||||
Sub: func (a, b int) int { | |||||
// Why isn't this built-in, come on Go | |||||
return a - b | |||||
}, | |||||
}) | }) | ||||
}) | }) | ||||
@@ -261,6 +261,11 @@ main.new-contact .actions > *:not(:last-child) { | |||||
margin-right: 1rem; | margin-right: 1rem; | ||||
} | } | ||||
.actions h3 { | |||||
align-self: center; | |||||
margin: 0 1.3rem 0 1rem; | |||||
} | |||||
.message-list-subject a { color: #77c; } | .message-list-subject a { color: #77c; } | ||||
.message-list-unread.message-list-sender, | .message-list-unread.message-list-sender, | ||||
@@ -370,7 +375,7 @@ main table tfoot { | |||||
padding: 0.3rem 0.5rem; | padding: 0.3rem 0.5rem; | ||||
} | } | ||||
.actions-pagination .button-link:first-child:not(:last-child) { | |||||
.actions-pagination .button-link:not(:last-child) { | |||||
margin-right: 0.3rem; | margin-right: 0.3rem; | ||||
} | } | ||||
@@ -444,6 +449,100 @@ main table tfoot { | |||||
margin-top: 1rem; | margin-top: 1rem; | ||||
} | } | ||||
main.calendar .dates { | |||||
flex-grow: 1; | |||||
padding: 0.3rem; | |||||
display: grid; | |||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr; | |||||
grid-template-rows: auto 1fr 1fr 1fr 1fr 1fr 1fr auto; | |||||
grid-template-areas: | |||||
"sunday-top monday-top wednesday-top tuesday-top thursday-top friday-top saturday-top" | |||||
"dates dates dates dates dates dates dates" | |||||
"dates dates dates dates dates dates dates" | |||||
"dates dates dates dates dates dates dates" | |||||
"dates dates dates dates dates dates dates" | |||||
"dates dates dates dates dates dates dates" | |||||
"dates dates dates dates dates dates dates" | |||||
"sunday-bottom monday-bottom wednesday-bottom tuesday-bottom thursday-bottom friday-bottom saturday-bottom"; | |||||
grid-gap: 0.3rem; | |||||
} | |||||
main.calendar .dates .weekday { | |||||
text-align: center; | |||||
font-size: 1.1rem; | |||||
font-weight: normal; | |||||
} | |||||
main.calendar .dates .sunday-top { grid-area: sunday-top; } | |||||
main.calendar .dates .monday-top { grid-area: monday-top; } | |||||
main.calendar .dates .tuesday-top { grid-area: tuesday-top; } | |||||
main.calendar .dates .wednesday-top { grid-area: wednesday-top; } | |||||
main.calendar .dates .thursday-top { grid-area: thursday-top; } | |||||
main.calendar .dates .friday-top { grid-area: friday-top; } | |||||
main.calendar .dates .saturday-top { grid-area: saturday-top; } | |||||
main.calendar .dates .sunday-bottom { grid-area: sunday-bottom; } | |||||
main.calendar .dates .monday-bottom { grid-area: monday-bottom; } | |||||
main.calendar .dates .tuesday-bottom { grid-area: tuesday-bottom; } | |||||
main.calendar .dates .wednesday-bottom { grid-area: wednesday-bottom; } | |||||
main.calendar .dates .thursday-bottom { grid-area: thursday-bottom; } | |||||
main.calendar .dates .friday-bottom { grid-area: friday-bottom; } | |||||
main.calendar .dates .saturday-bottom { grid-area: saturday-bottom; } | |||||
main.calendar .date { | |||||
border: 1px solid #eee; | |||||
padding: 0.3rem; | |||||
background: white; | |||||
display: flex; | |||||
flex-direction: column; | |||||
position: relative; | |||||
} | |||||
main.calendar .date.active { | |||||
background-color: #f6fff6; | |||||
border: 1px solid #afa; | |||||
} | |||||
main.calendar .date .date-link { | |||||
position: absolute; | |||||
top: 0; right: 0; bottom: 0; left: 0; | |||||
} | |||||
main.calendar .date.extra { | |||||
background: transparent; | |||||
border: none; | |||||
} | |||||
main.calendar .date ul { | |||||
list-style: none; | |||||
margin: 0; | |||||
padding-left: 0.3rem; | |||||
} | |||||
main.calendar .date .events { | |||||
flex-grow: 1; | |||||
} | |||||
main.calendar .date.extra .events { | |||||
visibility: hidden; | |||||
} | |||||
main.calendar .events .start-time { | |||||
color: #444; | |||||
} | |||||
main.calendar .events .overflow { | |||||
color: #444; | |||||
text-align: right; | |||||
} | |||||
main.calendar .date h4 { | |||||
font-weight: normal; | |||||
text-align: right; | |||||
color: #666; | |||||
} | |||||
main.calendar .date h4 .da { font-size: 1.2rem; } | |||||
input[type="submit"], | input[type="submit"], | ||||
.button, | .button, | ||||
button, | button, | ||||
@@ -0,0 +1,18 @@ | |||||
<div class="actions-wrap"> | |||||
<div class="actions-pagination" style="margin-left: 0;"> | |||||
<a href="?month={{.PrevPage}}" class="button-link">« {{.PrevTime.Format "January"}}</a> | |||||
<h3>{{.Time.Format "January 2006"}}</h3> | |||||
<a href="?month={{.NextPage}}" class="button-link">{{.NextTime.Format "January"}} »</a> | |||||
{{if ne .Time.Month .Now.Month}} | |||||
<a href="/calendar" class="button-link">Today »</a> | |||||
{{end}} | |||||
</div> | |||||
<form method="get" class="actions-search action-group"> | |||||
<input | |||||
type="text" | |||||
name="query" | |||||
placeholder="Search {{.Calendar.Name}} events..."> | |||||
<button>Search</button> | |||||
</form> | |||||
</div> |
@@ -0,0 +1,84 @@ | |||||
{{template "head.html" .}} | |||||
{{template "nav.html" .}} | |||||
<div class="page-wrap"> | |||||
<aside> | |||||
<a href="/calendar/create" class="new">New event</a> | |||||
<!-- TODO: fetch list of address books --> | |||||
<a href="#" class="active">{{.Calendar.Name}}</a> | |||||
<a href="#">Personal</a> | |||||
</aside> | |||||
<div class="container"> | |||||
<main class="calendar"> | |||||
<section class="actions"> | |||||
{{ template "calendar-header.html" . }} | |||||
</section> | |||||
<section class="dates"> | |||||
<h4 class="weekday sunday-top">Sunday</h4> | |||||
<h4 class="weekday monday-top">Monday</h4> | |||||
<h4 class="weekday tuesday-top">Tuesday</h4> | |||||
<h4 class="weekday wednesday-top">Wednesday</h4> | |||||
<h4 class="weekday thursday-top">Thursday</h4> | |||||
<h4 class="weekday friday-top">Friday</h4> | |||||
<h4 class="weekday saturday-top">Saturday</h4> | |||||
{{$base := .}} | |||||
{{range .Dates}} | |||||
<div class="date | |||||
{{if ne $base.Time.Month .Month}}extra{{end}} | |||||
{{if and (eq $base.Now.Month .Month) (eq $base.Now.Day .Day)}}active{{end}} | |||||
"> | |||||
{{if eq $base.Time.Month .Month}} | |||||
<a href="#" class="date-link"></a> | |||||
{{end}} | |||||
<div class="events"> | |||||
{{$events := (call $base.EventsForDate .)}} | |||||
{{if $events}} | |||||
<ul> | |||||
{{$overflow := 0}} | |||||
{{if gt (len $events) 3}} | |||||
{{$overflow = call $base.Sub (len $events) 3}} | |||||
{{$events = slice $events 0 3}} | |||||
{{end}} | |||||
{{range $events}} | |||||
{{$event := index .Data.Events 0}} | |||||
<li> | |||||
<span class="start-time"> | |||||
{{($event.DateTimeStart nil).Format "15:04"}} | |||||
</span> | |||||
{{$event.Props.Text "SUMMARY"}} | |||||
</li> | |||||
{{end}} | |||||
{{if ne $overflow 0}} | |||||
<li class="overflow">...and {{$overflow}} more</li> | |||||
{{end}} | |||||
</ul> | |||||
{{end}} | |||||
</div> | |||||
<h4> | |||||
<span class="mo">{{.Format "Jan"}}</span> | |||||
<span class="da">{{.Format "2"}}{{call $base.DaySuffix .Day}}</span> | |||||
</h4> | |||||
</div> | |||||
{{end}} | |||||
<h4 class="weekday sunday-bottom">Sunday</h4> | |||||
<h4 class="weekday monday-bottom">Monday</h4> | |||||
<h4 class="weekday tuesday-bottom">Tuesday</h4> | |||||
<h4 class="weekday wednesday-bottom">Wednesday</h4> | |||||
<h4 class="weekday thursday-bottom">Thursday</h4> | |||||
<h4 class="weekday friday-bottom">Friday</h4> | |||||
<h4 class="weekday saturday-bottom">Saturday</h4> | |||||
</section> | |||||
<section class="actions"> | |||||
{{ template "calendar-header.html" . }} | |||||
</section> | |||||
</main> | |||||
</div> | |||||
</div> | |||||
{{template "foot.html"}} |