develop #14
5 changed files with 610 additions and 0 deletions
|
|
@ -9,6 +9,7 @@ import (
|
|||
"math/rand/v2"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.smarteching.com/goffee/core"
|
||||
"git.smarteching.com/goffee/core/template/components"
|
||||
|
|
@ -861,3 +862,133 @@ func getPaginatedPageViews(values [][]components.ContentTableTD, limit int, offs
|
|||
return values[offset:end] // Slice the array
|
||||
|
||||
}
|
||||
|
||||
// Custom Templates functions
|
||||
func TemplatesFunctions(c *core.Context) *core.Response {
|
||||
|
||||
// check if template engine is enabled
|
||||
TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE")
|
||||
if TemplateEnableStr == "" {
|
||||
TemplateEnableStr = "false"
|
||||
}
|
||||
TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr)
|
||||
|
||||
if TemplateEnable {
|
||||
|
||||
tmplData := SamplePageData()
|
||||
return c.Response.Template("custom_templates_functions.html", tmplData)
|
||||
|
||||
} else {
|
||||
|
||||
message := "{\"message\": \"Error, template not enabled\"}"
|
||||
return c.Response.Json(message)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Author represents the writer of an article to show the custom functions
|
||||
type Author struct {
|
||||
Name string
|
||||
Avatar string
|
||||
Bio string
|
||||
}
|
||||
|
||||
// Article represents a blog post or news entry used to test template helpers to show the custom functions
|
||||
type Article struct {
|
||||
Title string
|
||||
Slug string
|
||||
Excerpt string
|
||||
Body string
|
||||
Tags []string
|
||||
PublishedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Author Author
|
||||
Views int
|
||||
Price float64
|
||||
Featured bool
|
||||
Subtitle string // intentionally left empty on some entries to test coalesce/defaultVal
|
||||
}
|
||||
|
||||
// PageData is the top-level context passed to the HTML template to show the custom functions
|
||||
type PageData struct {
|
||||
SiteTitle string
|
||||
Articles []Article
|
||||
}
|
||||
|
||||
// SamplePageData returns a populated PageData ready to be rendered by the template.
|
||||
func SamplePageData() PageData {
|
||||
return PageData{
|
||||
SiteTitle: "go/template lab",
|
||||
Articles: []Article{
|
||||
{
|
||||
Title: "getting started with go templates",
|
||||
Slug: "getting-started-go-templates",
|
||||
Excerpt: "Go's html/template package is both powerful and safe by default. In this article we explore how to extend it with custom FuncMap helpers that bring it closer to the expressiveness of Liquid or Twig, without sacrificing any of the security guarantees.",
|
||||
Tags: []string{"go", "templates", "web", "backend"},
|
||||
PublishedAt: time.Now().Add(-3 * 24 * time.Hour),
|
||||
UpdatedAt: time.Now().Add(-1 * 24 * time.Hour),
|
||||
Views: 14200,
|
||||
Price: 0,
|
||||
Featured: true,
|
||||
Subtitle: "",
|
||||
Author: Author{
|
||||
Name: "marina voss",
|
||||
Avatar: "MV",
|
||||
Bio: "Senior backend engineer focused on developer tooling and observability.",
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "building a blog engine in go",
|
||||
Slug: "blog-engine-go",
|
||||
Excerpt: "We walk through building a minimal but complete blog engine using only the Go standard library: routing with net/http, persistence with database/sql, and rendering with html/template.",
|
||||
Tags: []string{"go", "blog", "sqlite"},
|
||||
PublishedAt: time.Now().Add(-10 * 24 * time.Hour),
|
||||
UpdatedAt: time.Now().Add(-10 * 24 * time.Hour),
|
||||
Views: 8750,
|
||||
Price: 9.99,
|
||||
Featured: false,
|
||||
Subtitle: "A zero-dependency approach",
|
||||
Author: Author{
|
||||
Name: "rafael okonkwo",
|
||||
Avatar: "RO",
|
||||
Bio: "Full-stack engineer and open-source contributor. Writes about Go and distributed systems.",
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "concurrency patterns you should know",
|
||||
Slug: "concurrency-patterns-go",
|
||||
Excerpt: "Goroutines are cheap, but misusing them is expensive. This deep-dive covers fan-out/fan-in, pipelines, semaphores, and error group patterns with real production examples.",
|
||||
Tags: []string{"go", "concurrency", "goroutines", "advanced"},
|
||||
PublishedAt: time.Now().Add(-45 * 24 * time.Hour),
|
||||
UpdatedAt: time.Now().Add(-40 * 24 * time.Hour),
|
||||
Views: 31400,
|
||||
Price: 14.99,
|
||||
Featured: true,
|
||||
Subtitle: "",
|
||||
Author: Author{
|
||||
Name: "selin çelik",
|
||||
Avatar: "SÇ",
|
||||
Bio: "Systems programmer. Previously at Cloudflare. Loves writing about the internals of things.",
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "understanding go interfaces",
|
||||
Slug: "understanding-go-interfaces",
|
||||
Excerpt: "Interfaces in Go are implicit and structural, which makes them both elegant and occasionally surprising. We look at how the runtime dispatches method calls and how to design composable interfaces.",
|
||||
Tags: []string{"go", "interfaces", "design"},
|
||||
PublishedAt: time.Now().Add(-2 * time.Hour),
|
||||
UpdatedAt: time.Now().Add(-1 * time.Hour),
|
||||
Views: 420,
|
||||
Price: 0,
|
||||
Featured: false,
|
||||
Subtitle: "Implicit, structural, and powerful",
|
||||
Author: Author{
|
||||
Name: "marina voss",
|
||||
Avatar: "MV",
|
||||
Bio: "Senior backend engineer focused on developer tooling and observability.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
|
|
@ -21,9 +21,11 @@ require (
|
|||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
git.smarteching.com/zeni/go-chart/v2 v2.1.4 // indirect
|
||||
git.smarteching.com/zeni/go-charts/v2 v2.6.11 // indirect
|
||||
github.com/SparkPost/gosparkpost v0.2.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect
|
||||
github.com/go-sql-driver/mysql v1.10.0 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -4,6 +4,8 @@ git.smarteching.com/goffee/core v1.9.6 h1:GY1EXqbmBEWZAVrl3q22Izb6aXhQzFVQBv2hWh
|
|||
git.smarteching.com/goffee/core v1.9.6/go.mod h1:ifiBgTOR4zCMzdGsabNrEO792EHny2o149NGe3TSlms=
|
||||
git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q=
|
||||
git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ=
|
||||
git.smarteching.com/zeni/go-charts/v2 v2.6.11 h1:9udzlv3uxGXszpplfkL5IaTUrgkNj++KwhbaN1vVEqI=
|
||||
git.smarteching.com/zeni/go-charts/v2 v2.6.11/go.mod h1:3OpRPSXg7Qx4zcgsmwsC9ZFB9/wAkGSbnXf1wIbHYCg=
|
||||
github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s=
|
||||
github.com/SparkPost/gosparkpost v0.2.0/go.mod h1:S9WKcGeou7cbPpx0kTIgo8Q69WZvUmVeVzbD+djalJ4=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
|
|
@ -21,6 +23,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
|
|
|
|||
|
|
@ -60,4 +60,6 @@ func registerRoutes() {
|
|||
|
||||
controller.Get("/appsession", controllers.AppSession)
|
||||
controller.Post("/appsession", controllers.AppSession)
|
||||
|
||||
controller.Get("/templatesfunc", controllers.TemplatesFunctions)
|
||||
}
|
||||
|
|
|
|||
471
storage/templates/custom_templates_functions.html
Normal file
471
storage/templates/custom_templates_functions.html
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.SiteTitle}} — template lab</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0d0d0f;
|
||||
--surface: #141416;
|
||||
--border: #222226;
|
||||
--muted: #3a3a40;
|
||||
--subtle: #6b6b75;
|
||||
--body: #c8c8d0;
|
||||
--heading: #f0f0f4;
|
||||
--accent: #e8c547;
|
||||
--accent2: #5b8cff;
|
||||
--danger: #ff6b6b;
|
||||
--mono: 'JetBrains Mono', monospace;
|
||||
--serif: 'Playfair Display', Georgia, serif;
|
||||
--sans: 'DM Sans', sans-serif;
|
||||
}
|
||||
|
||||
html { background: var(--bg); color: var(--body); font-family: var(--sans); font-size: 16px; line-height: 1.6; }
|
||||
|
||||
body { max-width: 1100px; margin: 0 auto; padding: 0 2rem 6rem; }
|
||||
|
||||
/* ── Header ── */
|
||||
header {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 3rem 0 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
.site-title {
|
||||
font-family: var(--mono);
|
||||
font-size: .75rem;
|
||||
letter-spacing: .2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
}
|
||||
.site-headline {
|
||||
font-family: var(--serif);
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
color: var(--heading);
|
||||
line-height: 1.1;
|
||||
margin-top: .4rem;
|
||||
}
|
||||
.site-headline em { color: var(--accent); font-style: italic; }
|
||||
.helper-count {
|
||||
font-family: var(--mono);
|
||||
font-size: .7rem;
|
||||
color: var(--subtle);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Section label ── */
|
||||
.section-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 4rem 0 1.5rem;
|
||||
}
|
||||
.section-label span {
|
||||
font-family: var(--mono);
|
||||
font-size: .65rem;
|
||||
letter-spacing: .15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.section-label::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
/* ── Helper badge ── */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
font-family: var(--mono);
|
||||
font-size: .6rem;
|
||||
letter-spacing: .08em;
|
||||
padding: .15em .5em;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--muted);
|
||||
color: var(--subtle);
|
||||
vertical-align: middle;
|
||||
margin-left: .4rem;
|
||||
}
|
||||
.badge-accent { border-color: var(--accent); color: var(--accent); }
|
||||
.badge-blue { border-color: var(--accent2); color: var(--accent2); }
|
||||
|
||||
/* ── Demo block ── */
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1px;
|
||||
background: var(--border);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.demo-cell {
|
||||
background: var(--surface);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.demo-cell h3 {
|
||||
font-family: var(--mono);
|
||||
font-size: .7rem;
|
||||
letter-spacing: .1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.demo-cell .result {
|
||||
font-size: 1rem;
|
||||
color: var(--heading);
|
||||
}
|
||||
.demo-cell .note {
|
||||
margin-top: .5rem;
|
||||
font-size: .75rem;
|
||||
color: var(--subtle);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
/* ── Article cards ── */
|
||||
.article-list { display: flex; flex-direction: column; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
||||
|
||||
.article-card { background: var(--surface); padding: 2rem; display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: start; transition: background .15s; }
|
||||
.article-card:hover { background: #18181c; }
|
||||
|
||||
.card-meta { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin-bottom: .75rem; }
|
||||
.tag {
|
||||
font-family: var(--mono);
|
||||
font-size: .6rem;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
padding: .2em .55em;
|
||||
background: var(--muted);
|
||||
border-radius: 3px;
|
||||
color: var(--body);
|
||||
}
|
||||
.tag-featured { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
|
||||
|
||||
.card-title { font-family: var(--serif); font-size: 1.35rem; color: var(--heading); line-height: 1.25; margin-bottom: .5rem; }
|
||||
.card-subtitle { font-size: .85rem; color: var(--accent2); margin-bottom: .6rem; font-style: italic; }
|
||||
.card-excerpt { font-size: .875rem; color: var(--body); line-height: 1.65; }
|
||||
|
||||
.card-author { display: flex; align-items: center; gap: .6rem; margin-top: 1.25rem; }
|
||||
.avatar {
|
||||
width: 32px; height: 32px; border-radius: 50%;
|
||||
background: var(--muted);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: var(--mono); font-size: .55rem; color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.author-name { font-size: .8rem; color: var(--heading); }
|
||||
.author-date { font-size: .72rem; color: var(--subtle); font-family: var(--mono); }
|
||||
|
||||
.card-aside { text-align: right; }
|
||||
.views { font-family: var(--mono); font-size: .75rem; color: var(--subtle); white-space: nowrap; }
|
||||
.views strong { color: var(--body); display: block; font-size: 1rem; }
|
||||
.price-badge {
|
||||
display: inline-block; margin-top: .75rem;
|
||||
font-family: var(--mono); font-size: .7rem;
|
||||
padding: .3em .7em; border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--accent2) 15%, transparent);
|
||||
color: var(--accent2); border: 1px solid var(--accent2);
|
||||
}
|
||||
.price-free {
|
||||
background: color-mix(in srgb, #4caf50 12%, transparent);
|
||||
color: #6fcf97; border-color: #4caf50;
|
||||
}
|
||||
|
||||
/* ── Logic demo table ── */
|
||||
.logic-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
|
||||
.logic-table th {
|
||||
font-family: var(--mono); font-size: .65rem; letter-spacing: .1em; text-transform: uppercase;
|
||||
color: var(--subtle); text-align: left; padding: .6rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.logic-table td { padding: .75rem 1rem; border-bottom: 1px solid var(--border); color: var(--body); }
|
||||
.logic-table tr:last-child td { border-bottom: none; }
|
||||
.logic-table tr:hover td { background: var(--surface); }
|
||||
.val { font-family: var(--mono); font-size: .8rem; color: var(--accent); }
|
||||
|
||||
/* ── Footer ── */
|
||||
footer {
|
||||
margin-top: 5rem; padding-top: 2rem; border-top: 1px solid var(--border);
|
||||
font-family: var(--mono); font-size: .65rem; color: var(--muted);
|
||||
display: flex; justify-content: space-between; flex-wrap: wrap; gap: .5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
HEADER
|
||||
════════════════════════════════════════════ -->
|
||||
<header>
|
||||
<div>
|
||||
<p class="site-title">{{.SiteTitle}}</p>
|
||||
<h1 class="site-headline">Template <em>helpers</em><br>in action</h1>
|
||||
</div>
|
||||
<p class="helper-count">22 helpers registered<br>across 6 groups</p>
|
||||
</header>
|
||||
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
STRING HELPERS
|
||||
════════════════════════════════════════════ -->
|
||||
<div class="section-label"><span>String helpers</span></div>
|
||||
|
||||
{{$first := first .Articles}}
|
||||
|
||||
<div class="demo-grid">
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>capitalize <span class="badge">capitalize</span></h3>
|
||||
<p class="result">{{capitalize $first.Title}}</p>
|
||||
<p class="note">input: "{{$first.Title}}"</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>truncate <span class="badge">truncate</span></h3>
|
||||
<p class="result">{{$first.Excerpt | truncate 80}}</p>
|
||||
<p class="note">capped at 80 chars</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>prepend <span class="badge">prepend</span></h3>
|
||||
<p class="result">{{prepend $first.Slug "/articles/"}}</p>
|
||||
<p class="note">prepended "/articles/" to slug</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>strAppend <span class="badge">strAppend</span></h3>
|
||||
<p class="result">{{strAppend $first.Slug ".html"}}</p>
|
||||
<p class="note">appended ".html" to slug</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>split → join <span class="badge">split</span> <span class="badge">join</span></h3>
|
||||
{{$parts := split "go,templates,funcmap" ","}}
|
||||
<p class="result">{{join $parts " · "}}</p>
|
||||
<p class="note">split on "," then joined with " · "</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
NUMBER & DATE HELPERS
|
||||
════════════════════════════════════════════ -->
|
||||
<div class="section-label"><span>Number & Date helpers</span></div>
|
||||
|
||||
<div class="demo-grid">
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>fmtNumber — int <span class="badge">fmtNumber</span></h3>
|
||||
<p class="result">{{fmtNumber $first.Views}}</p>
|
||||
<p class="note">raw value: {{$first.Views}}</p>
|
||||
</div>
|
||||
|
||||
{{$paid := index .Articles 2}}
|
||||
<div class="demo-cell">
|
||||
<h3>fmtNumber — float <span class="badge">fmtNumber</span></h3>
|
||||
<p class="result">$ {{fmtNumber $paid.Price}}</p>
|
||||
<p class="note">raw value: {{$paid.Price}}</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>fmtDate "short" <span class="badge">fmtDate</span></h3>
|
||||
<p class="result">{{fmtDate $first.PublishedAt "short"}}</p>
|
||||
<p class="note">layout: "02 Jan 2006"</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>fmtDate "long" <span class="badge">fmtDate</span></h3>
|
||||
<p class="result">{{fmtDate $first.PublishedAt "long"}}</p>
|
||||
<p class="note">layout: "02 January 2006"</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>fmtDate "iso" <span class="badge">fmtDate</span></h3>
|
||||
<p class="result">{{fmtDate $first.PublishedAt "iso"}}</p>
|
||||
<p class="note">layout: "2006-01-02"</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>timeAgo <span class="badge">timeAgo</span></h3>
|
||||
{{range .Articles}}
|
||||
<p class="result" style="margin-bottom:.3rem">{{timeAgo .PublishedAt}} <span class="note" style="display:inline">— {{fmtDate .PublishedAt "short"}}</span></p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
COLLECTION HELPERS
|
||||
════════════════════════════════════════════ -->
|
||||
<div class="section-label"><span>Collection helpers</span></div>
|
||||
|
||||
<div class="demo-grid">
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>first <span class="badge">first</span></h3>
|
||||
{{with first .Articles}}
|
||||
<p class="result">{{capitalize .Title}}</p>
|
||||
<p class="note">first article in the list</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>last <span class="badge">last</span></h3>
|
||||
{{with last .Articles}}
|
||||
<p class="result">{{capitalize .Title}}</p>
|
||||
<p class="note">last article in the list</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>sliceOf 0–2 <span class="badge">sliceOf</span></h3>
|
||||
{{range sliceOf .Articles 0 2}}
|
||||
<p class="result" style="margin-bottom:.25rem">— {{capitalize .Title}}</p>
|
||||
{{end}}
|
||||
<p class="note">only first 2 of {{len .Articles}} articles</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>contains — slice <span class="badge">contains</span></h3>
|
||||
{{if contains $first.Tags "go"}}
|
||||
<p class="result" style="color:var(--accent)">✓ has tag "go"</p>
|
||||
{{else}}
|
||||
<p class="result" style="color:var(--danger)">✗ missing tag "go"</p>
|
||||
{{end}}
|
||||
<p class="note">tags: {{join $first.Tags ", "}}</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>contains — string <span class="badge">contains</span></h3>
|
||||
{{if contains $first.Excerpt "FuncMap"}}
|
||||
<p class="result" style="color:var(--accent)">✓ excerpt mentions "FuncMap"</p>
|
||||
{{else}}
|
||||
<p class="result" style="color:var(--danger)">✗ not found</p>
|
||||
{{end}}
|
||||
<p class="note">substring search on .Excerpt</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-cell">
|
||||
<h3>join <span class="badge">join</span></h3>
|
||||
<p class="result">{{join $first.Tags " / "}}</p>
|
||||
<p class="note">tags joined with " / "</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
LOGIC HELPERS
|
||||
════════════════════════════════════════════ -->
|
||||
<div class="section-label"><span>Logic helpers</span></div>
|
||||
|
||||
<table class="logic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Helper</th>
|
||||
<th>Article</th>
|
||||
<th>Input</th>
|
||||
<th>Output</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Articles}}
|
||||
<tr>
|
||||
<td><span class="badge badge-accent">defaultVal</span></td>
|
||||
<td>{{.Title | capitalize | truncate 30}}</td>
|
||||
<td class="val">.Subtitle ({{if .Subtitle}}"{{.Subtitle}}"{{else}}empty{{end}})</td>
|
||||
<td class="val">{{defaultVal "No subtitle" .Subtitle}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge badge-blue">ternary</span></td>
|
||||
<td>{{capitalize .Title | truncate 30}}</td>
|
||||
<td class="val">.Featured = {{.Featured}}</td>
|
||||
<td class="val">{{ternary "⭐ featured" "regular" .Featured}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge badge-accent">coalesce</span></td>
|
||||
<td>{{capitalize .Title | truncate 30}}</td>
|
||||
<td class="val">.Subtitle → .Title</td>
|
||||
<td class="val">{{coalesce .Subtitle .Title}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
FULL ARTICLE CARDS (all helpers combined)
|
||||
════════════════════════════════════════════ -->
|
||||
<div class="section-label"><span>Full article cards — all helpers combined</span></div>
|
||||
|
||||
<div class="article-list">
|
||||
{{range .Articles}}
|
||||
<div class="article-card">
|
||||
<div>
|
||||
<div class="card-meta">
|
||||
{{if .Featured}}<span class="tag tag-featured">featured</span>{{end}}
|
||||
{{range .Tags}}<span class="tag">{{.}}</span>{{end}}
|
||||
</div>
|
||||
|
||||
<h2 class="card-title">{{capitalize .Title}}</h2>
|
||||
|
||||
{{with coalesce .Subtitle ""}}
|
||||
<p class="card-subtitle">{{.}}</p>
|
||||
{{end}}
|
||||
|
||||
<p class="card-excerpt">{{.Excerpt | truncate 160}}</p>
|
||||
|
||||
<a style="font-family:var(--mono);font-size:.7rem;color:var(--accent2);text-decoration:none;margin-top:.75rem;display:inline-block;"
|
||||
href="{{prepend .Slug "/articles/"}}">
|
||||
{{prepend .Slug "/articles/"}} →
|
||||
</a>
|
||||
|
||||
<div class="card-author">
|
||||
<div class="avatar">{{.Author.Avatar}}</div>
|
||||
<div>
|
||||
<div class="author-name">{{capitalize .Author.Name}}</div>
|
||||
<div class="author-date">
|
||||
{{fmtDate .PublishedAt "short"}} · {{timeAgo .PublishedAt}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-aside">
|
||||
<div class="views">
|
||||
<strong>{{fmtNumber .Views}}</strong>
|
||||
views
|
||||
</div>
|
||||
{{if gt .Price 0.0}}
|
||||
<span class="price-badge">$ {{fmtNumber .Price}}</span>
|
||||
{{else}}
|
||||
<span class="price-badge price-free">free</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
FOOTER
|
||||
════════════════════════════════════════════ -->
|
||||
<footer>
|
||||
<span>{{.SiteTitle}} / template sandbox</span>
|
||||
<span>{{len .Articles}} articles · {{fmtDate (first .Articles).PublishedAt "iso"}} → {{fmtDate (last .Articles).PublishedAt "iso"}}</span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue