@@ -18,9 +18,16 @@ import ( | |||
type CalendarRenderData struct { | |||
alps.BaseRenderData | |||
Time time.Time | |||
Now time.Time | |||
Dates [7 * 6]time.Time | |||
Calendar *caldav.Calendar | |||
Events []CalendarObject | |||
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 { | |||
@@ -96,13 +103,66 @@ func registerRoutes(p *alps.GoPlugin, u *url.URL) { | |||
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{ | |||
BaseRenderData: *alps.NewBaseRenderData(ctx), | |||
Time: start, | |||
Now: time.Now(), // TODO: Use client time zone | |||
Calendar: calendar, | |||
Dates: dates, | |||
Events: newCalendarObjectList(events), | |||
PrevPage: 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; | |||
} | |||
.actions h3 { | |||
align-self: center; | |||
margin: 0 1.3rem 0 1rem; | |||
} | |||
.message-list-subject a { color: #77c; } | |||
.message-list-unread.message-list-sender, | |||
@@ -370,7 +375,7 @@ main table tfoot { | |||
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; | |||
} | |||
@@ -444,6 +449,100 @@ main table tfoot { | |||
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"], | |||
.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"}} |