Merge pull request 'develop' (#15) from develop into main
Reviewed-on: #15
This commit is contained in:
commit
06c4d5765a
12 changed files with 2 additions and 1857 deletions
|
|
@ -1,52 +0,0 @@
|
|||
// Copyright (c) 2025 Zeni Kim <zenik@smarteching.com>
|
||||
// Use of this source code is governed by MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.smarteching.com/goffee/core"
|
||||
"git.smarteching.com/goffee/cup/workers"
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
// Make samples queues
|
||||
func Queuesample(c *core.Context) *core.Response {
|
||||
|
||||
// Get client queue asynq
|
||||
client := c.GetQueueClient()
|
||||
|
||||
// Create a task with typename and payload.
|
||||
payload, err := json.Marshal(workers.EmailTaskPayload{UserID: 42})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
t1 := asynq.NewTask(workers.TypeWelcomeEmail, payload)
|
||||
|
||||
t2 := asynq.NewTask(workers.TypeReminderEmail, payload)
|
||||
|
||||
// Process the task immediately.
|
||||
info, err := client.Enqueue(t1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf(" [*] Successfully enqueued task: %+v", info)
|
||||
|
||||
// Process 2 task 1 min later.
|
||||
for i := 1; i < 3; i++ {
|
||||
info, err = client.Enqueue(t2, asynq.ProcessIn(1*time.Minute))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf(" [*] Successfully enqueued task: %+v", info)
|
||||
}
|
||||
|
||||
message := "{\"message\": \"Task queued\"}"
|
||||
return c.Response.Json(message)
|
||||
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
|
||||
// Use of this source code is governed by MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.smarteching.com/goffee/core"
|
||||
"git.smarteching.com/goffee/core/template/components"
|
||||
"git.smarteching.com/goffee/cup/utils"
|
||||
)
|
||||
|
||||
// Show basic template
|
||||
func Sample(c *core.Context) *core.Response {
|
||||
|
||||
// first, include all compoments
|
||||
type templateData struct {
|
||||
PageCard components.PageCard
|
||||
}
|
||||
|
||||
// now fill data of the components
|
||||
tmplData := templateData{
|
||||
PageCard: components.PageCard{
|
||||
CardTitle: "Framework Goffee",
|
||||
CardBody: "Powered by Golang",
|
||||
},
|
||||
}
|
||||
|
||||
return c.Response.Template("basic.html", tmplData)
|
||||
|
||||
}
|
||||
|
||||
// Show basic app login
|
||||
func AppLogin(c *core.Context) *core.Response {
|
||||
|
||||
// first, include all compoments
|
||||
// first, include all compoments
|
||||
type templateData struct {
|
||||
PageCard components.PageCard
|
||||
}
|
||||
|
||||
// now fill data of the components
|
||||
tmplData := templateData{
|
||||
PageCard: components.PageCard{
|
||||
CardTitle: "Card title",
|
||||
CardBody: "Loerm ipsum at deim",
|
||||
},
|
||||
}
|
||||
return c.Response.Template("login.html", tmplData)
|
||||
}
|
||||
|
||||
// Show basic app login
|
||||
func AppSession(c *core.Context) *core.Response {
|
||||
|
||||
var session = new(utils.SessionUser)
|
||||
|
||||
// true if session is active
|
||||
hassession := session.Init(c)
|
||||
|
||||
//session.Set("numberdos", 66)
|
||||
|
||||
type templateData struct {
|
||||
PageCard components.PageCard
|
||||
}
|
||||
|
||||
// now fill data of the components
|
||||
tmplData := templateData{}
|
||||
|
||||
if hassession {
|
||||
|
||||
sesiondata := ""
|
||||
cardtitle := fmt.Sprintf("Session user id: %v", session.GetUserID())
|
||||
numberdos, ok := session.Get("numberdos")
|
||||
|
||||
if numberdos != nil {
|
||||
numberdos = numberdos.(float64) + 10
|
||||
} else {
|
||||
numberdos = 10
|
||||
}
|
||||
|
||||
session.Set("numberdos", numberdos)
|
||||
|
||||
if ok {
|
||||
sesiondata = fmt.Sprintf("OK, Session numberdos has %v", numberdos)
|
||||
} else {
|
||||
sesiondata = fmt.Sprintf("No ok, session numberdos has %v", numberdos)
|
||||
}
|
||||
|
||||
// delete single
|
||||
//session.Delete("numberdos")
|
||||
|
||||
// delete all data
|
||||
//session.Flush()
|
||||
|
||||
tmplData = templateData{
|
||||
PageCard: components.PageCard{
|
||||
CardTitle: cardtitle,
|
||||
CardBody: sesiondata,
|
||||
},
|
||||
}
|
||||
|
||||
return c.Response.Template("appsession.html", tmplData)
|
||||
|
||||
} else {
|
||||
|
||||
return c.Response.Template("login.html", tmplData)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Show basic app sample
|
||||
func AppSample(c *core.Context) *core.Response {
|
||||
|
||||
// first, include all compoments
|
||||
type templateData struct {
|
||||
PageCard components.PageCard
|
||||
ContentDropdown components.ContentDropdown
|
||||
}
|
||||
|
||||
// now fill data of the components
|
||||
tmplData := templateData{
|
||||
PageCard: components.PageCard{
|
||||
CardTitle: "Protected page",
|
||||
CardBody: "If you can see this page, your are loggedin",
|
||||
},
|
||||
ContentDropdown: components.ContentDropdown{
|
||||
Label: "dropdown",
|
||||
Items: []components.ContentDropdownItem{
|
||||
{
|
||||
Text: "Signout",
|
||||
Link: "#",
|
||||
ID: "signout",
|
||||
},
|
||||
{
|
||||
Text: "item disabled",
|
||||
Link: "#",
|
||||
IsDisabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
//fmt.Printf("Outside cookie user is: %s", user.Email)
|
||||
|
||||
return c.Response.Template("app.html", tmplData)
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
26
routes.go
26
routes.go
|
|
@ -8,7 +8,6 @@ package main
|
|||
import (
|
||||
"git.smarteching.com/goffee/core"
|
||||
"git.smarteching.com/goffee/cup/controllers"
|
||||
"git.smarteching.com/goffee/cup/hooks"
|
||||
)
|
||||
|
||||
// Register the app controllers
|
||||
|
|
@ -20,15 +19,8 @@ func registerRoutes() {
|
|||
|
||||
// Define your routes here...
|
||||
controller.Get("/", controllers.WelcomeHome)
|
||||
// Uncomment the lines below to enable theme demo
|
||||
controller.Get("/themebase", controllers.Themedemo)
|
||||
controller.Get("/themeform", controllers.Themeform)
|
||||
controller.Get("/themecontent", controllers.Themecontent)
|
||||
controller.Get("/themepanel", controllers.Themedemo)
|
||||
controller.Get("/themeelements", controllers.ThemeElements)
|
||||
controller.Get("/queuesample", controllers.Queuesample)
|
||||
|
||||
// Uncomment the lines below to enable authentication
|
||||
// Uncomment the lines below to enable authentication API
|
||||
controller.Post("/signup", controllers.Signup)
|
||||
controller.Post("/signin", controllers.Signin)
|
||||
controller.Post("/signout", controllers.Signout)
|
||||
|
|
@ -44,22 +36,6 @@ func registerRoutes() {
|
|||
controller.Post("/admin/users/edit/:id", controllers.AdminUsersEdit)
|
||||
controller.Post("/admin/users/delete", controllers.AdminUsersDelete)
|
||||
controller.Post("/admin/users/deleteconfirm", controllers.AdminUsersDelConfirm)
|
||||
//controller.Get("/admin/users/roles", controllers.Signout)
|
||||
//controller.Get("/admin/users/permissions", controllers.ResetPasswordRequest)
|
||||
|
||||
controller.Get("/dashboard", controllers.WelcomeToDashboard, hooks.AuthCheck)
|
||||
|
||||
// templates demos
|
||||
controller.Get("/signout", controllers.Signout)
|
||||
|
||||
controller.Get("/appsample", controllers.AppSample, hooks.AuthCheck)
|
||||
controller.Post("/appsample", controllers.AppSample, hooks.AuthCheck)
|
||||
|
||||
controller.Get("/applogin", controllers.AppLogin, hooks.CheckSessionCookie)
|
||||
controller.Post("/applogin", controllers.AppLogin, hooks.CheckSessionCookie)
|
||||
|
||||
controller.Get("/appsession", controllers.AppSession)
|
||||
controller.Post("/appsession", controllers.AppSession)
|
||||
|
||||
controller.Get("/templatesfunc", controllers.TemplatesFunctions)
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "page_head" "Sample page"}}
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{{template "content_dropdown" .ContentDropdown}}
|
||||
{{template "page_card" .PageCard}}
|
||||
{{ define "page_card_content" }}
|
||||
|
||||
<img class="goffeelogo"src="/public/img/goffee.png" alt="Goffee logo" />
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{template "page_footer"}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "page_head" "Sample page test session vars"}}
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{{template "page_card" .PageCard}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "page_footer"}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,471 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "page_head" "Goffee"}}
|
||||
<body>
|
||||
<div class="container">
|
||||
{{template "page_card" .PageCard}}
|
||||
</div>
|
||||
{{template "page_footer"}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "page_head" "Goffee"}}
|
||||
<body>
|
||||
<div class="container">
|
||||
<fieldset>
|
||||
<legend>Content demos</legend>
|
||||
<div class="row">
|
||||
{{template "content_table" .ContentTable}}
|
||||
{{if .ShouldShowPagination}}
|
||||
{{template "content_pagination" .Pagination}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<h2>Pie chart</h2>
|
||||
{{template "content_graph" .ContentGraph}}
|
||||
<hr>
|
||||
{{template "form_input" .FieldText}}
|
||||
{{template "form_select" .FormSelectCityM}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{template "content_tabledetail" .ContentTabledetail}}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{{template "page_footer"}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "page_head" "Goffee"}}
|
||||
<body>
|
||||
<div class="container">
|
||||
<fieldset class="mb-3">
|
||||
<legend>Demos buttons</legend>
|
||||
<div class="container border rounded-3 p-2">
|
||||
{{range .Buttons}}
|
||||
{{template "form_button" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend>Demos href</legend>
|
||||
<div class="container d-flex justify-content-between border rounded-3 p-2">
|
||||
{{range .Hrefs}}
|
||||
{{template "content_href" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend>Demos Badges</legend>
|
||||
<div class="container d-flex justify-content-between border rounded-3 p-2">
|
||||
{{range .Badges}}
|
||||
{{template "content_badge" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend>Demos dropdown</legend>
|
||||
<div class="container d-flex justify-content-between border rounded-3 p-2">
|
||||
{{range .Dropdowns}}
|
||||
{{template "content_dropdown" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend>Demos List</legend>
|
||||
<div class="container d-flex justify-content-between border rounded-3 p-2">
|
||||
{{range .Lists}}
|
||||
{{template "content_list" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend>Demos nav</legend>
|
||||
<div class="container border rounded-3 p-2 ">
|
||||
{{range .Menus}}
|
||||
<div class="container border rounded-3 p-2 mb-2">
|
||||
{{template "page_nav" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{{template "page_footer"}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "page_head" "Goffee"}}
|
||||
<body>
|
||||
<div class="container">
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>form demos</legend>
|
||||
<div class="row">
|
||||
{{template "form_input" .FormText}}
|
||||
{{template "form_input" .FormEmail}}
|
||||
{{template "form_select" .FormSelectCity}}
|
||||
{{template "form_textarea" .FormTextarea}}
|
||||
{{template "form_radio" .FormRadio}}
|
||||
{{template "form_checkbox" .FormCheckbox}}
|
||||
</div>
|
||||
</fieldset>
|
||||
{{template "form_button" .FormButton}}
|
||||
</form>
|
||||
</div>
|
||||
{{template "page_footer"}}
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue