commit - 93ff9d7f210e985d4d61bdb8dda1ea69c2b2cb43
commit + c4a56c0a8e7accfd981a9b7b19515d5f3302c278
blob - e03df411766d7530d1f97f11d077a14853a3aee0
blob + 99641078e9a491a9611743912649a41225603666
--- internal/service/handlers.go
+++ internal/service/handlers.go
Results []rss.Channel
}
+var templateFuncs = template.FuncMap{
+ "timeAgo": timeAgo,
+}
+
func render(w http.ResponseWriter, name string, data any) {
- tmpl := template.Must(template.ParseGlob("internal/templates/*.tmpl"))
+ tmpl := template.New("").Funcs(templateFuncs)
+ tmpl = template.Must(tmpl.ParseGlob("internal/templates/*.tmpl"))
tmpl.ExecuteTemplate(w, name, data)
}
blob - /dev/null
blob + b207f5c5d52efde9845648e85728ecb33f499fb8 (mode 644)
--- /dev/null
+++ internal/service/util.go
+package service
+
+import (
+ "fmt"
+ "time"
+)
+
+func timeAgo(t time.Time) string {
+ duration := time.Since(t)
+
+ switch {
+ case duration < time.Minute:
+ return "just now"
+ case duration < time.Hour:
+ mins := int(duration.Minutes())
+ if mins == 1 {
+ return "1 minute ago"
+ }
+ return fmt.Sprintf("%d minutes ago", mins)
+ case duration < 24*time.Hour:
+ hrs := int(duration.Hours())
+ if hrs == 1 {
+ return "1 hour ago"
+ }
+ return fmt.Sprintf("%d hours ago", hrs)
+ case duration < 365*24*time.Hour:
+ days := int(duration.Hours()) / 24
+ if days == 1 {
+ return "1 day ago"
+ }
+ return fmt.Sprintf("%d days ago", days)
+ default:
+ years := int(duration.Hours()) / 24 / 365
+ if years == 1 {
+ return "1 year ago"
+ }
+ return fmt.Sprintf("%d years ago", years)
+ }
+}
blob - d74a5939f20dbb31284e8318c329c0f2bace4fc1
blob + 19559976b5fedec3fdbb7b5a905a98abe09800a9
--- internal/templates/results.tmpl
+++ internal/templates/results.tmpl
<div class="results">
{{ range . }}
<div class="result">
- <img src="{{ .Image.Href }}" />
+ <img src="{{ .Image.Href }}" width="170" height="170" />
<div>
- <h3>{{ .Title }}</h3>
+ <hgroup>
+ <h2>{{ .Title }}</h2>
+ <time datetime="{{ .PubDate.Format "2006-01-02T15:04:05Z07:00" }}">
+ Last updated {{ .PubDate | timeAgo }}
+ </time>
+ </hgroup>
<p>{{ .Description }}</p>
- {{ range .Link }}
- <a href="{{.}}">{{ . }}</a>
- {{ end }}
- <!-- <div>Copyright - {{ .Copyright }}</div> -->
- <div>PubDate - {{ .PubDate }}</div>
- <div>Items - {{ len .Items }}</div>
- {{ range .Categories }}
- <div>Category - {{ .Text }}</div>
- {{ end }}
- <div>Owner - {{ .Owner.Email }}</div>
- <!-- <div>Explicit - {{ .Explicit }}</div> -->
+ <div class="tag-line">
+ {{ if gt (len .Link) 0 }}
+ <a href="{{index .Link 0}}" class="button">
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-globe2" viewBox="0 0 16 16">
+ <path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855q-.215.403-.395.872c.705.157 1.472.257 2.282.287zM4.249 3.539q.214-.577.481-1.078a7 7 0 0 1 .597-.933A7 7 0 0 0 3.051 3.05q.544.277 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9 9 0 0 1-1.565-.667A6.96 6.96 0 0 0 1.018 7.5zm1.4-2.741a12.3 12.3 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332M8.5 5.09V7.5h2.99a12.3 12.3 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.6 13.6 0 0 1 7.5 10.91V8.5zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741zm-3.282 3.696q.18.469.395.872c.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a7 7 0 0 1-.598-.933 9 9 0 0 1-.481-1.079 8.4 8.4 0 0 0-1.198.49 7 7 0 0 0 2.276 1.522zm-1.383-2.964A13.4 13.4 0 0 1 3.508 8.5h-2.49a6.96 6.96 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667m6.728 2.964a7 7 0 0 0 2.275-1.521 8.4 8.4 0 0 0-1.197-.49 9 9 0 0 1-.481 1.078 7 7 0 0 1-.597.933M8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855q.216-.403.395-.872A12.6 12.6 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.96 6.96 0 0 0 14.982 8.5h-2.49a13.4 13.4 0 0 1-.437 3.008M14.982 7.5a6.96 6.96 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008zM11.27 2.461q.266.502.482 1.078a8.4 8.4 0 0 0 1.196-.49 7 7 0 0 0-2.275-1.52c.218.283.418.597.597.932m-.488 1.343a8 8 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"/>
+ </svg>
+ </a>
+ {{ end }}
+ <a href="mailto:{{.Owner.Email}}" class="button" style="margin-right: 1rem">
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-envelope" viewBox="0 0 16 16">
+ <path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1zm13 2.383-4.708 2.825L15 11.105zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741M1 11.105l4.708-2.897L1 5.383z"/>
+ </svg>
+ </a>
+
+ {{ range .Categories }}
+ <a class="category">#{{ .Text }}</a>
+ {{ end }}
+ </div>
</div>
</div>
{{ end }}
blob - b3c398b78b58e18fc417a6119e785b66f98aa4f5
blob + f49b78523709356d5ea4991e57b1a5c47965d94c
--- web/style.css
+++ web/style.css
--pink: #ee8695;
--blue: #57c2ff;
--dark-blue: #468fb9;
+ --light-gray: #919191;
--gray: #35343d;
--dark-gray: #292831;
--black: #1c1b1e;
&.search-area {
margin: 0 auto;
width: 100%;
- max-width: 700px;
+ max-width: 900px;
}
}
margin-bottom: 1rem;
img {
- width: 200px;
- height: 200px;
border-radius: 2rem;
margin-right: 1rem;
+ }
+
+ hgroup {
+ margin-bottom: 0.5rem;
+
+ h2 {
+ margin-bottom: 0;
+ }
+
+ time {
+ color: var(--light-gray);
+ font-size: .8em;
+ }
+ }
+
+ a {
+ font-size: .9em;
+ }
+
+ .tag-line {
+ display: flex;
+ align-items: center;
+
+ a.button {
+ margin-right: .5rem;
+ display: block;
+ color: var(--black);
+ padding: 4px;
+ border-radius: 100px;
+ line-height: 0;
+ background-color: white;
+ }
+
+ a.category {
+ margin-right: .5rem;
+ background-color: var(--dark-blue);
+ color: white;
+ font-size: .6em;
+ padding: .2rem .4rem;
+ border-radius: 1rem;
+ }
}
}
\ No newline at end of file