hswaw/site: add checkinator integration

Change-Id: I19a72da67410332d6d82d49e3a54f1dc0f81ff65
diff --git a/hswaw/site/BUILD.bazel b/hswaw/site/BUILD.bazel
index 21edded..a34f5b4 100644
--- a/hswaw/site/BUILD.bazel
+++ b/hswaw/site/BUILD.bazel
@@ -4,6 +4,7 @@
 go_library(
     name = "go_default_library",
     srcs = [
+        "at.go",
         "feeds.go",
         "main.go",
         "views.go",
diff --git a/hswaw/site/at.go b/hswaw/site/at.go
new file mode 100644
index 0000000..70df8c7
--- /dev/null
+++ b/hswaw/site/at.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+const (
+	atURL = "https://at.hackerspace.pl/api"
+)
+
+// atStatus is the result of queruing checkinator/at (Hackerspace presence
+// service).
+type atStatus struct {
+	// Users is the list of present and publicly visible users.
+	Users []atUser `json:"users"`
+	// ESPs is the number of ESP{8266,32} devices.
+	ESPs int `json:"esps"`
+	// Kektops is the number of nettop “Kektop” devices.
+	Kektops int `json:"kektops"`
+	// Unknown is the number of unknown devices in the network.
+	Unknown int `json:"unknown"`
+}
+
+type atUser struct {
+	Login string `json:"login"`
+}
+
+func getAt(ctx context.Context) (*atStatus, error) {
+	r, err := http.NewRequestWithContext(ctx, "GET", atURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("NewRequest(%q): %w", atURL, err)
+	}
+	res, err := http.DefaultClient.Do(r)
+	if err != nil {
+		return nil, fmt.Errorf("GET: %w", err)
+	}
+	defer res.Body.Close()
+
+	var status atStatus
+	if err := json.NewDecoder(res.Body).Decode(&status); err != nil {
+		return nil, fmt.Errorf("when decoding JSON: %w", err)
+	}
+
+	return &status, nil
+}
diff --git a/hswaw/site/static/landing.css b/hswaw/site/static/landing.css
index 431d8ea..3ddae1a 100644
--- a/hswaw/site/static/landing.css
+++ b/hswaw/site/static/landing.css
@@ -141,9 +141,19 @@
     opacity: 60%;
 }
 
-#quicklinks {
-    float: right;
-    font-family: monospace;
-    font-size: 14px;
-    margin: 2rem;
+.atlist {
+    display: inline;
+    list-style: none;
+}
+
+.atlist li {
+    display: inline;
+}
+
+.atlist li:after {
+    content: ", ";
+}
+
+.atlist li:last-child:after {
+    content: ".";
 }
diff --git a/hswaw/site/templates/index.html b/hswaw/site/templates/index.html
index 48f0acd..058c162 100644
--- a/hswaw/site/templates/index.html
+++ b/hswaw/site/templates/index.html
@@ -36,6 +36,28 @@
             <p>
               <b>Hackerspace nie zna barier.</b> Jeśli masz ciekawy pomysł i szukasz ludzi chętnych do współpracy lub po prostu potrzebujesz miejsca i sprzętu - <a href="">zapraszamy</a>!
             </p>
+            <h3>Kto jest teraz w spejsie?</h3>
+            <p>
+                {{ if ne .AtError nil }}
+                <i>Ups, nie udało się załadować stanu checkinatora.</i>
+                {{ else }}
+                  {{ $count := len .AtStatus.Users }}
+                  {{ if gt $count 4 }}
+                Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie znajduje się {{ $count }} osób:
+                  {{ else if gt $count 1 }}
+                Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie znajdują się {{ $count }} osoby:
+                  {{ else if gt $count 0 }}
+                Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie znajduje się jedna osoba:
+                  {{ else }}
+                Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie nie ma nikogo.
+                  {{ end }}
+                <ul class="atlist">
+                    {{ range .AtStatus.Users }}
+                    <li>{{ .Login }}</li>
+                    {{ end }}
+                </ul>
+                {{ end }}
+            </p>
         </div>
         <div class="blog">
             <h2>Blog</h2>
diff --git a/hswaw/site/views.go b/hswaw/site/views.go
index 59f948e..ef1f1fa 100644
--- a/hswaw/site/views.go
+++ b/hswaw/site/views.go
@@ -53,7 +53,13 @@
 
 // handleIndex handles rendering the main page at /.
 func (s *service) handleIndex(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+
+	atStatus, atError := getAt(ctx)
+
 	render(w, tmplIndex, map[string]interface{}{
-		"Entries": s.getFeeds(),
+		"Entries":  s.getFeeds(),
+		"AtStatus": atStatus,
+		"AtError":  atError,
 	})
 }