b8146ae485
Add minimum quotas for users to earn the ability to create custom forums. The entry requirements that could earn the first forum include: 1. Having a Certified account status for at least 45 days. 2. Having written 10 posts or replies in the forums. Additional quota is granted in increasing difficulty based on the count of forum posts created. Other changes: * Admin view of Manage Forums can filter for official/community. * "Certified Since" now shown on profile pages. * Update FAQ page for Forums feature.
324 lines
8.3 KiB
Go
324 lines
8.3 KiB
Go
package templates
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
|
"code.nonshy.com/nonshy/website/pkg/markdown"
|
|
"code.nonshy.com/nonshy/website/pkg/models"
|
|
"code.nonshy.com/nonshy/website/pkg/photo"
|
|
"code.nonshy.com/nonshy/website/pkg/session"
|
|
"code.nonshy.com/nonshy/website/pkg/utility"
|
|
"golang.org/x/text/language"
|
|
"golang.org/x/text/message"
|
|
)
|
|
|
|
// Generics
|
|
type Number interface {
|
|
int | int64 | uint64 | float32 | float64
|
|
}
|
|
|
|
// TemplateFuncs available to all pages.
|
|
func TemplateFuncs(r *http.Request) template.FuncMap {
|
|
return template.FuncMap{
|
|
"InputCSRF": InputCSRF(r),
|
|
"SincePrettyCoarse": SincePrettyCoarse(),
|
|
"FormatNumberShort": FormatNumberShort(),
|
|
"FormatNumberCommas": FormatNumberCommas(),
|
|
"ComputeAge": utility.Age,
|
|
"Split": strings.Split,
|
|
"ToMarkdown": ToMarkdown,
|
|
"ToJSON": ToJSON,
|
|
"ToHTML": ToHTML,
|
|
"PhotoURL": photo.URLPath,
|
|
"Now": time.Now,
|
|
"RunTime": RunTime,
|
|
"PrettyTitle": func() template.HTML {
|
|
return template.HTML(fmt.Sprintf(
|
|
`<strong style="color: #0077FF">non</strong>` +
|
|
`<strong style="color: #FF77FF">shy</strong>`,
|
|
))
|
|
},
|
|
"PrettyTitleShort": func() template.HTML {
|
|
return template.HTML(`<strong style="color: #0077FF">n</strong>` +
|
|
`<strong style="color: #FF77FF">s</strong>`,
|
|
)
|
|
},
|
|
"Pluralize": Pluralize[int],
|
|
"Pluralize64": Pluralize[int64],
|
|
"PluralizeU64": Pluralize[uint64],
|
|
"Substring": Substring,
|
|
"TrimEllipses": TrimEllipses,
|
|
"IterRange": IterRange,
|
|
"SubtractInt": SubtractInt,
|
|
"SubtractInt64": SubtractInt64,
|
|
"UrlEncode": UrlEncode,
|
|
"QueryPlus": QueryPlus(r),
|
|
"SimplePager": SimplePager(r),
|
|
"HasSuffix": strings.HasSuffix,
|
|
|
|
// Test if a photo should be blurred ({{BlurExplicit .Photo}})
|
|
"BlurExplicit": BlurExplicit(r),
|
|
|
|
// Get a description for an admin scope (e.g. for transparency page).
|
|
"AdminScopeDescription": config.AdminScopeDescription,
|
|
}
|
|
}
|
|
|
|
// InputCSRF returns the HTML snippet for a CSRF token hidden input field.
|
|
func InputCSRF(r *http.Request) func() template.HTML {
|
|
return func() template.HTML {
|
|
ctx := r.Context()
|
|
if token, ok := ctx.Value(session.CSRFKey).(string); ok {
|
|
return template.HTML(fmt.Sprintf(
|
|
`<input type="hidden" name="%s" value="%s">`,
|
|
config.CSRFInputName,
|
|
token,
|
|
))
|
|
} else {
|
|
return template.HTML(`[CSRF middleware error]`)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RunTime returns the elapsed time between the HTTP request start and now, as a formatted string.
|
|
func RunTime(r *http.Request) string {
|
|
if rt, ok := r.Context().Value(session.RequestTimeKey).(time.Time); ok {
|
|
duration := time.Since(rt)
|
|
return duration.Round(time.Millisecond).String()
|
|
}
|
|
return "ERROR"
|
|
}
|
|
|
|
// BlurExplicit returns true if the current user has the blur_explicit setting on and the given Photo is Explicit.
|
|
func BlurExplicit(r *http.Request) func(*models.Photo) bool {
|
|
return func(photo *models.Photo) bool {
|
|
if !photo.Explicit {
|
|
return false
|
|
}
|
|
|
|
currentUser, err := session.CurrentUser(r)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return currentUser.GetProfileField("blur_explicit") == "true"
|
|
}
|
|
}
|
|
|
|
// SincePrettyCoarse formats a time.Duration in plain English. Intended for "joined 2 months ago" type
|
|
// strings - returns the coarsest level of granularity.
|
|
func SincePrettyCoarse() func(time.Time) template.HTML {
|
|
return func(since time.Time) template.HTML {
|
|
return template.HTML(utility.FormatDurationCoarse(time.Since(since)))
|
|
}
|
|
}
|
|
|
|
// FormatNumberShort will format any integer number into a short representation.
|
|
func FormatNumberShort() func(v interface{}) template.HTML {
|
|
return func(v interface{}) template.HTML {
|
|
var number int64
|
|
switch t := v.(type) {
|
|
case int:
|
|
number = int64(t)
|
|
case int64:
|
|
number = int64(t)
|
|
case uint:
|
|
number = int64(t)
|
|
case uint64:
|
|
number = int64(t)
|
|
case float32:
|
|
number = int64(t)
|
|
case float64:
|
|
number = int64(t)
|
|
default:
|
|
return template.HTML("#INVALID#")
|
|
}
|
|
return template.HTML(utility.FormatNumberShort(number))
|
|
}
|
|
}
|
|
|
|
// FormatNumberCommas will pretty print a long number by adding commas.
|
|
func FormatNumberCommas() func(v interface{}) template.HTML {
|
|
return func(v interface{}) template.HTML {
|
|
var number int64
|
|
switch t := v.(type) {
|
|
case int:
|
|
number = int64(t)
|
|
case int64:
|
|
number = int64(t)
|
|
case uint:
|
|
number = int64(t)
|
|
case uint64:
|
|
number = int64(t)
|
|
case float32:
|
|
number = int64(t)
|
|
case float64:
|
|
number = int64(t)
|
|
default:
|
|
return template.HTML("#INVALID#")
|
|
}
|
|
p := message.NewPrinter(language.English)
|
|
return template.HTML(p.Sprintf("%d", number))
|
|
}
|
|
}
|
|
|
|
// ToMarkdown renders input text as Markdown.
|
|
func ToMarkdown(input string) template.HTML {
|
|
return template.HTML(markdown.Render(input))
|
|
}
|
|
|
|
// ToHTML renders input text as trusted HTML code.
|
|
func ToHTML(input string) template.HTML {
|
|
return template.HTML(input)
|
|
}
|
|
|
|
// ToJSON will stringify any json-serializable object.
|
|
func ToJSON(v any) template.JS {
|
|
bin, err := json.Marshal(v)
|
|
if err != nil {
|
|
return template.JS(err.Error())
|
|
}
|
|
return template.JS(string(bin))
|
|
}
|
|
|
|
// Pluralize text based on a quantity number. Provide up to 2 labels for the
|
|
// singular and plural cases, or the defaults are "", "s"
|
|
func Pluralize[V Number](count V, labels ...string) string {
|
|
if len(labels) < 2 {
|
|
labels = []string{"", "s"}
|
|
}
|
|
|
|
if count == 1 {
|
|
return labels[0]
|
|
}
|
|
return labels[1]
|
|
}
|
|
|
|
// Substring safely returns the first N characters of a string.
|
|
func Substring(value string, n int) string {
|
|
if n > len(value) {
|
|
return value
|
|
}
|
|
return value[:n]
|
|
}
|
|
|
|
// TrimEllipses is like Substring but will add an ellipses if truncated.
|
|
func TrimEllipses(value string, n int) string {
|
|
if n > len(value) {
|
|
return value
|
|
}
|
|
return value[:n] + "…"
|
|
}
|
|
|
|
// IterRange returns a list of integers useful for pagination.
|
|
func IterRange(start, n int) []int {
|
|
var result = []int{}
|
|
for i := start; i <= n; i++ {
|
|
result = append(result, i)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// SubtractInt subtracts two numbers.
|
|
func SubtractInt(a, b int) int {
|
|
return a - b
|
|
}
|
|
|
|
// SubtractInt64 subtracts two numbers.
|
|
func SubtractInt64(a, b int64) int64 {
|
|
return a - b
|
|
}
|
|
|
|
// UrlEncode escapes a series of values (joined with no delimiter)
|
|
func UrlEncode(values ...interface{}) string {
|
|
var result string
|
|
for _, value := range values {
|
|
result += url.QueryEscape(fmt.Sprintf("%v", value))
|
|
}
|
|
return result
|
|
}
|
|
|
|
// QueryPlus takes the current request's query parameters and upserts them with new values.
|
|
//
|
|
// Use it like: {{QueryPlus "page" .NextPage}}
|
|
//
|
|
// Returns the query string sans the ? prefix, like "key1=value1&key2=value2"
|
|
func QueryPlus(r *http.Request) func(...interface{}) template.URL {
|
|
return func(upsert ...interface{}) template.URL {
|
|
// Get current parameters.
|
|
// Note: COPY them from r.Form so we don't accidentally modify r.Form.
|
|
var params = map[string][]string{}
|
|
for k, v := range r.Form {
|
|
params[k] = v
|
|
}
|
|
|
|
// Mix in the incoming fields.
|
|
for i := 0; i < len(upsert); i += 2 {
|
|
var (
|
|
key = fmt.Sprintf("%v", upsert[i])
|
|
value interface{}
|
|
)
|
|
if len(upsert) > i {
|
|
value = upsert[i+1]
|
|
}
|
|
|
|
params[key] = []string{fmt.Sprintf("%v", value)}
|
|
}
|
|
|
|
// Assemble and return the query string.
|
|
var parts = []string{}
|
|
for k, vs := range params {
|
|
for _, v := range vs {
|
|
parts = append(parts,
|
|
fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(v)),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Sort them deterministically.
|
|
sort.Strings(parts)
|
|
|
|
return template.URL(strings.Join(parts, "&"))
|
|
}
|
|
}
|
|
|
|
// SimplePager creates a paginator row (partial template).
|
|
//
|
|
// Use it like: {{SimplePager .Pager}}
|
|
//
|
|
// It runs the template partial 'simple_pager.html' to customize it for the site theme.
|
|
func SimplePager(r *http.Request) func(*models.Pagination) template.HTML {
|
|
return func(pager *models.Pagination) template.HTML {
|
|
tmpl, err := template.New("index").Funcs(template.FuncMap{
|
|
"QueryPlus": QueryPlus(r),
|
|
}).ParseFiles(config.TemplatePath + "/partials/simple_pager.html")
|
|
if err != nil {
|
|
return template.HTML(err.Error())
|
|
}
|
|
|
|
var (
|
|
vars = struct {
|
|
Pager *models.Pagination
|
|
Request *http.Request
|
|
}{pager, r}
|
|
|
|
buf = bytes.NewBuffer([]byte{})
|
|
)
|
|
|
|
err = tmpl.ExecuteTemplate(buf, "SimplePager", vars)
|
|
if err != nil {
|
|
return template.HTML(err.Error())
|
|
}
|
|
return template.HTML(buf.String())
|
|
}
|
|
}
|