summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke T. Shumaker <lukeshu@lukeshu.com>2024-04-20 22:58:30 -0600
committerLuke T. Shumaker <lukeshu@lukeshu.com>2024-04-20 22:58:30 -0600
commit1ca444d3e659b61317ea62588930a0a5156934c5 (patch)
treeecedc3e82487439d2e5da29f8f752b4049661a9e
parent0856e4aab26f88ab0573f9227561ea0d80bcb352 (diff)
imworkingon: Initial go at daily statuses
-rw-r--r--cmd/generate/calendar.go122
-rw-r--r--cmd/generate/imworkingon.html.tmpl72
-rw-r--r--cmd/generate/main.go40
-rw-r--r--cmd/generate/src_mastodon.go52
-rw-r--r--public/imworkingon/imworkingon.scss94
5 files changed, 367 insertions, 13 deletions
diff --git a/cmd/generate/calendar.go b/cmd/generate/calendar.go
new file mode 100644
index 0000000..29c3318
--- /dev/null
+++ b/cmd/generate/calendar.go
@@ -0,0 +1,122 @@
+package main
+
+import (
+ "time"
+)
+
+//////////////////////////////////////////////////////////////////////
+
+type Date struct {
+ Year int
+ Month time.Month
+ Day int
+}
+
+func DateOf(t time.Time) Date {
+ y, m, d := t.Date()
+ return Date{Year: y, Month: m, Day: d}
+}
+
+func (d Date) Time() time.Time {
+ return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, time.Local)
+}
+
+func (d Date) AddDays(delta int) Date {
+ return DateOf(d.Time().AddDate(0, 0, delta))
+}
+
+func (d Date) Weekday() time.Weekday {
+ return d.Time().Weekday()
+}
+
+func (a Date) Cmp(b Date) int {
+ switch {
+ case a.Year < b.Year:
+ return -1
+ case a.Year > b.Year:
+ return 1
+ }
+ switch {
+ case a.Month < b.Month:
+ return -1
+ case a.Month > b.Month:
+ return 1
+ }
+ switch {
+ case a.Day < b.Day:
+ return -1
+ case a.Day > b.Day:
+ return 1
+ }
+ return 0
+}
+
+//////////////////////////////////////////////////////////////////////
+
+type CalendarDay[T any] struct {
+ Date
+ Data T
+}
+
+//////////////////////////////////////////////////////////////////////
+
+// keyed by time.Weekday
+type CalendarWeek[T any] [7]CalendarDay[T]
+
+//////////////////////////////////////////////////////////////////////
+
+// must be sorted, must be non-sparse
+type Calendar[T any] []CalendarWeek[T]
+
+func (c Calendar[T]) NumWeekdaysInMonth(weekday time.Weekday, target Date) int {
+ num := 0
+ for _, w := range c {
+ if w[weekday].Date == (Date{}) {
+ continue
+ }
+ switch {
+ case w[weekday].Year == target.Year:
+ switch {
+ case w[weekday].Month == target.Month:
+ num++
+ case w[weekday].Month > target.Month:
+ return num
+ }
+ case w[weekday].Year > target.Year:
+ return num
+ }
+ }
+ return num
+}
+
+//////////////////////////////////////////////////////////////////////
+
+func BuildCalendar[T any](things []T, dateOfThing func(T) Date) Calendar[T] {
+ if len(things) == 0 {
+ return nil
+ }
+
+ newestDate := DateOf(time.Now().Local())
+
+ oldestDate := dateOfThing(things[0])
+ byDate := make(map[Date]T, len(things))
+ for _, thing := range things {
+ date := dateOfThing(thing)
+ if oldestDate.Cmp(date) > 0 {
+ oldestDate = date
+ }
+ byDate[date] = thing
+ }
+
+ var ret Calendar[T]
+ for date := oldestDate; date.Cmp(newestDate) <= 0; date = date.AddDays(1) {
+ if len(ret) == 0 || date.Weekday() == 0 {
+ ret = append(ret, CalendarWeek[T]{})
+ }
+ ret[len(ret)-1][date.Weekday()] = CalendarDay[T]{
+ Date: date,
+ Data: byDate[date],
+ }
+ }
+ return ret
+}
diff --git a/cmd/generate/imworkingon.html.tmpl b/cmd/generate/imworkingon.html.tmpl
index fb24ac6..1be3960 100644
--- a/cmd/generate/imworkingon.html.tmpl
+++ b/cmd/generate/imworkingon.html.tmpl
@@ -81,6 +81,78 @@
</article>
{{- end }}
</section>
+ <section id="standups">
+ <h2>Daily statuses</h2>
+ <p>Posted daily on <a href="https://fosstodon.org/@lukeshu">Mastodon</a>.</p>
+
+ <details><summary>Calendar view</summary>
+ <table>
+ <thead>
+ <tr>
+ <th></th>
+ <th><abbr title="Sunday">Su</abbr></th>
+ <th><abbr title="Monday">M</abbr></th>
+ <th><abbr title="Tuesday">Tu</abbr></th>
+ <th><abbr title="Wednesday">W</abbr></th>
+ <th><abbr title="Thursday">Th</abbr></th>
+ <th><abbr title="Friday">F</abbr></th>
+ <th><abbr title="Saturday">S</abbr></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {{- $cal := .StandupCalendar }}
+ {{- $curSunMonth := 0 }}
+ {{- $curSatMonth := 0 }}
+ {{- range $i, $week := reverse .StandupCalendar }}
+ <tr>
+ {{- $sun := (index $week time.Sunday) }}
+ {{- if not $sun.Day }}
+ <th></th>
+ {{- else if ne $sun.Month $curSunMonth }}
+ <th class="{{ monthClass $sun.Month }}" rowspan="{{ $cal.NumWeekdaysInMonth time.Sunday $sun.Date }}">
+ <span>{{ $sun.Month }} {{ $sun.Year }}</span>
+ </th>
+ {{- $curSunMonth = $sun.Month }}
+ {{- end }}
+ {{- range $day := $week }}
+ {{- if not $day.Day }}
+ <td></td>
+ {{- else if not $day.Data }}
+ <td class="{{ monthClass $day.Month }}">
+ {{ $day.Day }}
+ </td>
+ {{- else }}
+ <td class="{{ monthClass $day.Month }}">
+ <a href="#standup-id-{{ $day.Data.ID }}">
+ {{ $day.Day }}
+ </a>
+ </td>
+ {{- end }}
+ </td>
+ {{- end }}
+ {{- $sat := (index $week time.Saturday) }}
+ {{- if not $sat.Day }}
+ <th></th>
+ {{- else if ne $sat.Month $curSatMonth }}
+ <th class="{{ monthClass $sat.Month }}" rowspan="{{ $cal.NumWeekdaysInMonth time.Saturday $sat.Date }}">
+ <span>{{ $sat.Month }} {{ $sat.Year }}</span>
+ </th>
+ {{- $curSatMonth = $sat.Month }}
+ {{- end }}
+ {{- end }}
+ </tr>
+ </tbody>
+ </table>
+ </details>
+
+ {{- range $status := .Standups }}
+ <article class="standup" id="standup-id-{{ $status.ID }}">
+ <div class="standup-title"><a href="{{ $status.URL }}">{{ timeTag $status.CreatedAt "2006-01-02" }}</a></div>
+ <div class="standup-content">{{ $status.Content }}</div>
+ </article>
+ {{- end }}
+ </section>
<footer>
<p>The content of this page is Copyright © Luke T. Shumaker.</p>
diff --git a/cmd/generate/main.go b/cmd/generate/main.go
index 249e2a5..dd226ad 100644
--- a/cmd/generate/main.go
+++ b/cmd/generate/main.go
@@ -5,6 +5,7 @@ import (
_ "embed"
"fmt"
"os"
+ "reflect"
"sort"
"strings"
"time"
@@ -40,6 +41,10 @@ var timeTagTmpl = template.Must(template.New("time.tag.tmpl").
Parse(`<time datetime="{{ .Machine }}" title="{{ .HumanVerbose }}">{{ .HumanPretty }}</time>`))
func mainWithError() error {
+ standups, err := ReadStandups("https://fosstodon.org", "lukeshu")
+ if err != nil {
+ return err
+ }
contribs, err := ReadContribs("imworkingon/contribs.yml")
if err != nil {
return err
@@ -67,6 +72,26 @@ func mainWithError() error {
tmpl := template.Must(template.New("imworkingon.html.tmpl").
Funcs(template.FuncMap{
+ "time": func() map[string]time.Weekday {
+ return map[string]time.Weekday{
+ "Sunday": time.Sunday,
+ "Monday": time.Monday,
+ "Tuesday": time.Tuesday,
+ "Wednesday": time.Wednesday,
+ "Thursday": time.Thursday,
+ "Friday": time.Friday,
+ "Saturday": time.Saturday,
+ }
+ },
+ "reverse": func(x any) any {
+ in := reflect.ValueOf(x)
+ l := in.Len()
+ out := reflect.MakeSlice(in.Type(), l, l)
+ for i := 0; i < l; i++ {
+ out.Index(l - (i + 1)).Set(in.Index(i))
+ }
+ return out.Interface()
+ },
"timeTag": func(ts time.Time, prettyFmt string) (template.HTML, error) {
ts = ts.Local()
var out strings.Builder
@@ -77,6 +102,13 @@ func mainWithError() error {
})
return template.HTML(out.String()), err
},
+ "monthClass": func(m time.Month) string {
+ if m%2 == 0 {
+ return "even-month"
+ } else {
+ return "odd-month"
+ }
+ },
"md2html": MarkdownToHTML,
"getUpstream": func(c Contribution) Upstream {
// First try any of the documented upstreams.
@@ -99,9 +131,11 @@ func mainWithError() error {
Parse(htmlTmplStr))
var out bytes.Buffer
if err := tmpl.Execute(&out, map[string]any{
- "Contribs": contribs,
- "Tags": tags,
- "Upstreams": upstreams,
+ "Contribs": contribs,
+ "Tags": tags,
+ "Upstreams": upstreams,
+ "Standups": standups,
+ "StandupCalendar": BuildCalendar(standups, func(status *MastodonStatus) Date { return DateOf(status.CreatedAt) }),
}); err != nil {
return err
}
diff --git a/cmd/generate/src_mastodon.go b/cmd/generate/src_mastodon.go
new file mode 100644
index 0000000..42ae8b2
--- /dev/null
+++ b/cmd/generate/src_mastodon.go
@@ -0,0 +1,52 @@
+package main
+
+import (
+ "html/template"
+ "net/url"
+ "slices"
+ "time"
+)
+
+type MastodonStatus struct {
+ ID string `json:"id"`
+ CreatedAt time.Time `json:"created_at"`
+ URL string `json:"url"`
+ Content template.HTML `json:"content"`
+}
+
+// Returns statuses sorted from newest to oldest.
+func ReadStandups(server, username string) ([]*MastodonStatus, error) {
+ var account struct {
+ ID string `json:"id"`
+ }
+ if err := httpGetJSON(server+"/api/v1/accounts/lookup?acct="+username, &account); err != nil {
+ return nil, err
+ }
+ var statuses []*MastodonStatus
+ for {
+ params := make(url.Values)
+ params.Set("tagged", "DailyStandUp")
+ params.Set("exclude_reblogs", "true")
+ if len(statuses) > 0 {
+ params.Set("max_id", statuses[len(statuses)-1].ID)
+ }
+ var resp []*MastodonStatus
+ if err := httpGetJSON(server+"/api/v1/accounts/"+account.ID+"/statuses?"+params.Encode(), &resp); err != nil {
+ return nil, err
+ }
+ if len(resp) == 0 {
+ break
+ }
+ statuses = append(statuses, resp...)
+ }
+
+ ignoreList := []string{
+ "https://fosstodon.org/@lukeshu/112198267818432116",
+ "https://fosstodon.org/@lukeshu/112198241414760456",
+ }
+ statuses = slices.DeleteFunc(statuses, func(status *MastodonStatus) bool {
+ return slices.Contains(ignoreList, status.URL)
+ })
+
+ return statuses, nil
+}
diff --git a/public/imworkingon/imworkingon.scss b/public/imworkingon/imworkingon.scss
index 55f7f85..4021fda 100644
--- a/public/imworkingon/imworkingon.scss
+++ b/public/imworkingon/imworkingon.scss
@@ -9,13 +9,20 @@ article {
border: solid 1px #333333;
border-radius: 1em;
margin: 0.5em;
+ overflow: hidden;
+}
+
+@mixin color-panel {
+ background-color: #DDDDFF;
+ border: solid 1px #8D8DA6;
+ margin: -1px;
}
-div {
- & > p:first-child {
+div > p {
+ &:first-child {
margin-top: 0;
}
- & > p:last-child {
+ &:last-child {
margin-bottom: 0;
}
}
@@ -45,18 +52,11 @@ article.contrib {
"uname tag tag tag"
"udesc desc desc desc";
gap: 1px;
- overflow: hidden;
& > div {
padding: 0.5em;
}
- @mixin color-panel {
- background-color: #DDDDFF;
- border: solid 1px #8D8DA6;
- margin: -1px;
- }
-
div.contrib-upstream-name {
grid-area: uname;
@include color-panel;
@@ -106,3 +106,77 @@ article.contrib {
}
&.released-contrib div.contrib-status { background-color: #1f883d; color: white; }
}
+
+article.standup {
+ div {
+ padding: 0.5em 1em;
+ }
+ div.standup-title {
+ @include color-panel;
+ text-align: center;
+ }
+}
+
+section#standups {
+ & > p, summary {
+ text-align: center;
+ }
+ table {
+ font-size: small;
+ }
+ td {
+ width: 2em;
+ text-align: right;
+ }
+ td.odd-month { background-color: #FFDDDD; }
+ td.odd-month:has(a) { background-color: #FFDDFF; }
+ th.odd-month { background-color: #EECCCC; }
+ td.even-month { background-color: #DDFFDD; }
+ td.even-month:has(a) { background-color: #DDFFFF; }
+ th.even-month { background-color: #CCEECC; }
+
+ th.even-month, th.odd-month {
+ width: 3em;
+ max-width: 3em;
+ span {
+ display: inline-block;
+ transform-origin: 0 50%;
+ }
+ &:first-child span { transform: rotate(-90deg) translateX(-50%) translateY(50%); }
+ &:last-child span { transform: rotate(90deg) translateX(-50%) translateY(-50%); }
+ }
+}
+
+@media (min-width: 8.5in) {
+ body {
+ display: grid;
+ grid-template-columns: 1fr 3.4in;
+ grid-template-rows:
+ min-content
+ min-content
+ min-content
+ min-content
+ 1fr
+ min-content;
+ grid-template-areas:
+ "head1 head1"
+ "head2 head2"
+ "tags standups"
+ "contribs-pending standups"
+ "contribs-completed standups"
+ "foot foot";
+ }
+ body > header { grid-area: head1; }
+ section#intro { grid-area: head2; }
+ section#tags { grid-area: tags; }
+ section#contribs-pending { grid-area: contribs-pending; }
+ section#contribs-completed { grid-area: contribs-completed; }
+ section#standups { grid-area: standups; max-width: 3.2in; }
+ body > footer { grid-area: foot; }
+
+ section#tags, section#standups {
+ & > h2:first-child {
+ margin-top: 0;
+ }
+ }
+}