add opentelemetry tracing
This commit is contained in:
@ -3,5 +3,4 @@ package config
|
||||
type HTTP struct {
|
||||
ListenAddr string `json:"listen_addr"`
|
||||
MountLive bool `json:"mount_live"`
|
||||
Engine string `json:"engine"`
|
||||
}
|
||||
|
||||
6
internal/common/config/trace.go
Normal file
6
internal/common/config/trace.go
Normal file
@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
type Trace struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
LicenseKey string `json:"license_key"`
|
||||
}
|
||||
@ -2,11 +2,24 @@ package decorator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
commandAttribute = attribute.Key("command_name")
|
||||
queryAttribute = attribute.Key("query_name")
|
||||
argsAttribute = attribute.Key("args")
|
||||
|
||||
apiTracer = otel.Tracer("cq")
|
||||
)
|
||||
|
||||
type commandLoggingDecorator[T any] struct {
|
||||
@ -18,6 +31,17 @@ func (c commandLoggingDecorator[T]) Handle(ctx context.Context, cmd T) (err erro
|
||||
handlerName := getTypeName[T]()
|
||||
|
||||
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName))
|
||||
|
||||
var argsBuilder strings.Builder
|
||||
_ = json.NewEncoder(&argsBuilder).Encode(cmd)
|
||||
|
||||
var span trace.Span
|
||||
ctx, span = apiTracer.Start(ctx, handlerName)
|
||||
span.SetAttributes(
|
||||
commandAttribute.String(handlerName),
|
||||
argsAttribute.String(argsBuilder.String()),
|
||||
)
|
||||
|
||||
xcontext.LogDebug(ctx, c.log, "executing command")
|
||||
start := time.Now()
|
||||
|
||||
@ -27,7 +51,9 @@ func (c commandLoggingDecorator[T]) Handle(ctx context.Context, cmd T) (err erro
|
||||
xcontext.LogInfo(ctx, c.log, "command executed successfuly", elapsed)
|
||||
} else {
|
||||
xcontext.LogError(ctx, c.log, "command execution failed", elapsed, slog.Any("err", err))
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
return c.base.Handle(ctx, cmd)
|
||||
@ -41,6 +67,17 @@ type queryLoggingDecorator[Q, U any] struct {
|
||||
func (q queryLoggingDecorator[Q, U]) Handle(ctx context.Context, query Q) (entity U, err error) {
|
||||
handlerName := getTypeName[Q]()
|
||||
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName))
|
||||
|
||||
var argsBuilder strings.Builder
|
||||
_ = json.NewEncoder(&argsBuilder).Encode(query)
|
||||
|
||||
var span trace.Span
|
||||
ctx, span = apiTracer.Start(ctx, handlerName)
|
||||
span.SetAttributes(
|
||||
queryAttribute.String(handlerName),
|
||||
argsAttribute.String(argsBuilder.String()),
|
||||
)
|
||||
|
||||
xcontext.LogDebug(ctx, q.log, "executing command")
|
||||
start := time.Now()
|
||||
|
||||
@ -50,7 +87,10 @@ func (q queryLoggingDecorator[Q, U]) Handle(ctx context.Context, query Q) (entit
|
||||
xcontext.LogInfo(ctx, q.log, "command executed successfuly", elapsed)
|
||||
} else {
|
||||
xcontext.LogError(ctx, q.log, "command execution failed", elapsed, slog.Any("err", err))
|
||||
span.RecordError(err)
|
||||
}
|
||||
now := time.Now()
|
||||
span.End(trace.WithTimestamp(now))
|
||||
}()
|
||||
|
||||
return q.base.Handle(ctx, query)
|
||||
|
||||
@ -6,6 +6,16 @@ import (
|
||||
)
|
||||
|
||||
type ctxLogKey struct{}
|
||||
type ctxRequestID struct{}
|
||||
|
||||
func WithRequestID(ctx context.Context, requestID string) context.Context {
|
||||
return context.WithValue(ctx, ctxRequestID{}, requestID)
|
||||
}
|
||||
|
||||
func GetRequestID(ctx context.Context) string {
|
||||
reqid, _ := ctx.Value(ctxRequestID{}).(string)
|
||||
return reqid
|
||||
}
|
||||
|
||||
type ctxLogAttrStore struct {
|
||||
attrs []slog.Attr
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
package xslices
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func ForEach[T any](items []T, f func(T)) {
|
||||
for _, item := range items {
|
||||
f(item)
|
||||
@ -13,3 +18,12 @@ func AsMap[T any, U comparable](items []T, f func(T) U) map[U]struct{} {
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func Shuffle[T any](items []T) {
|
||||
maxnum := big.NewInt(int64(len(items)))
|
||||
for i := range items {
|
||||
swapWith, _ := rand.Int(rand.Reader, maxnum)
|
||||
swapWithIdx := int(swapWith.Int64())
|
||||
items[i], items[swapWithIdx] = items[swapWithIdx], items[i]
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,6 @@ templ buttonRedirect(id, title string, linkTo string) {
|
||||
|
||||
script onclickRedirect(id, to string) {
|
||||
document.getElementById(id).onclick = () => {
|
||||
location.href = to
|
||||
location.href = to
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,11 +106,11 @@ func buttonRedirect(id, title string, linkTo string) templ.Component {
|
||||
|
||||
func onclickRedirect(id, to string) templ.ComponentScript {
|
||||
return templ.ComponentScript{
|
||||
Name: `__templ_onclickRedirect_47ae`,
|
||||
Function: `function __templ_onclickRedirect_47ae(id, to){document.getElementById(id).onclick = () => {
|
||||
location.href = to
|
||||
Name: `__templ_onclickRedirect_5c43`,
|
||||
Function: `function __templ_onclickRedirect_5c43(id, to){document.getElementById(id).onclick = () => {
|
||||
location.href = to
|
||||
}}`,
|
||||
Call: templ.SafeScript(`__templ_onclickRedirect_47ae`, id, to),
|
||||
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_47ae`, id, to),
|
||||
Call: templ.SafeScript(`__templ_onclickRedirect_5c43`, id, to),
|
||||
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_5c43`, id, to),
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,31 +4,31 @@ import "path"
|
||||
import "strconv"
|
||||
|
||||
script breadcrumbsLoad() {
|
||||
const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
const lt = document.getElementById('learning-type-filter');
|
||||
const ct = document.getElementById('course-thematic-filter');
|
||||
const lt = document.getElementById('learning-type-filter');
|
||||
const ct = document.getElementById('course-thematic-filter');
|
||||
|
||||
const prefix = (lt !== null && lt.value !== '') ? `/courses/${lt.value}` : `/courses`;
|
||||
const out = (ct !== null && ct.value !== '') ? `${prefix}/${ct.value}` : prefix;
|
||||
const prefix = (lt !== null && lt.value !== '') ? `/courses/${lt.value}` : `/courses`;
|
||||
const out = (ct !== null && ct.value !== '') ? `${prefix}/${ct.value}` : prefix;
|
||||
|
||||
document.location.assign(out);
|
||||
return false;
|
||||
};
|
||||
document.location.assign(out);
|
||||
return false;
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ff = document.getElementById('filter-form');
|
||||
if (ff === null) return;
|
||||
ff.addEventListener('submit', formFilterOnSubmit);
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ff = document.getElementById('filter-form');
|
||||
if (ff === null) return;
|
||||
ff.addEventListener('submit', formFilterOnSubmit);
|
||||
});
|
||||
}
|
||||
|
||||
templ breadcrumbsItem(text, link string, isActive bool) {
|
||||
<li class={"breadcrumb-item", templ.KV("active", isActive)}>
|
||||
<li class={ "breadcrumb-item", templ.KV("active", isActive) }>
|
||||
if link != "" {
|
||||
<a
|
||||
href={templ.URL(link)}
|
||||
href={ templ.URL(link) }
|
||||
itemprop="url"
|
||||
aria-label="breadcrumb"
|
||||
>{ text }</a>
|
||||
@ -39,9 +39,8 @@ templ breadcrumbsItem(text, link string, isActive bool) {
|
||||
}
|
||||
|
||||
templ breadcrumNode(params BreadcrumbsParams) {
|
||||
// TODO: add divider to nav style
|
||||
<nav
|
||||
class={"mt-4", breadcrumbSymbol()}
|
||||
class={ "mt-4", breadcrumbSymbol() }
|
||||
aria-label="breadcrumbs"
|
||||
itemprop="breadcrumb"
|
||||
itemtype="https://schema.org/BreadcrumbList"
|
||||
@ -49,18 +48,16 @@ templ breadcrumNode(params BreadcrumbsParams) {
|
||||
>
|
||||
<ol class="breadcrumb">
|
||||
@breadcrumbsItem("Курсы", "/courses", params.ActiveLearningType.Empty())
|
||||
|
||||
if !params.ActiveLearningType.Empty() {
|
||||
if !params.ActiveLearningType.Empty() {
|
||||
@breadcrumbsItem(
|
||||
params.ActiveLearningType.Name,
|
||||
params.ActiveLearningType.Name,
|
||||
path.Join("/", "courses", params.ActiveLearningType.ID),
|
||||
params.ActiveCourseThematic.Empty(),
|
||||
)
|
||||
}
|
||||
|
||||
if !params.ActiveCourseThematic.Empty() {
|
||||
@breadcrumbsItem(
|
||||
params.ActiveCourseThematic.Name,
|
||||
params.ActiveCourseThematic.Name,
|
||||
path.Join("/", "courses", params.ActiveLearningType.ID, params.ActiveCourseThematic.ID),
|
||||
true,
|
||||
)
|
||||
@ -84,30 +81,28 @@ templ listCoursesSectionFilters(params FilterFormParams) {
|
||||
<div class="col-8">
|
||||
<form id="filter-form" class="input-group">
|
||||
<span class="input-group-text">Filter courses</span>
|
||||
|
||||
<select
|
||||
id="learning-type-filter"
|
||||
class={"form-select"}
|
||||
class={ "form-select" }
|
||||
>
|
||||
<option value="" selected?={params.ActiveLearningType.ID==""}>All</option>
|
||||
<option value="" selected?={ params.ActiveLearningType.ID=="" }>All</option>
|
||||
for _, learningType := range params.AvailableLearningTypes {
|
||||
<option
|
||||
selected?={params.ActiveLearningType.ID==learningType.ID}
|
||||
value={learningType.ID}
|
||||
>{learningType.Name}</option>
|
||||
selected?={ params.ActiveLearningType.ID==learningType.ID }
|
||||
value={ learningType.ID }
|
||||
>{ learningType.Name }</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<select
|
||||
id="course-thematic-filter"
|
||||
class={"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)}
|
||||
class={ "form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0) }
|
||||
>
|
||||
<option value="" selected?={params.ActiveLearningType.ID==""}>All</option>
|
||||
<option value="" selected?={ params.ActiveLearningType.ID=="" }>All</option>
|
||||
for _, courseThematic := range params.AvailableCourseThematics {
|
||||
<option
|
||||
selected?={params.ActiveCourseThematic.ID==courseThematic.ID}
|
||||
value={courseThematic.ID}
|
||||
>{courseThematic.Name}</option>
|
||||
selected?={ params.ActiveCourseThematic.ID==courseThematic.ID }
|
||||
value={ courseThematic.ID }
|
||||
>{ courseThematic.Name }</option>
|
||||
}
|
||||
</select>
|
||||
<button id="filter-course-thematic" class="btn btn-outline-secondary" type="submit">Go</button>
|
||||
@ -119,8 +114,7 @@ templ listCoursesSectionFilters(params FilterFormParams) {
|
||||
templ listCoursesLearning(containers []CategoryContainer) {
|
||||
for _, container := range containers {
|
||||
<section class="row first-class-group">
|
||||
<h1 class="title">{container.Name}</h1>
|
||||
|
||||
<h1 class="title">{ container.Name }</h1>
|
||||
for _, subcategory := range container.Subcategories {
|
||||
@listCoursesThematicRow(subcategory)
|
||||
}
|
||||
@ -128,12 +122,10 @@ templ listCoursesLearning(containers []CategoryContainer) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
templ listCoursesThematicRow(subcategory SubcategoryContainer) {
|
||||
<div class="block second-class-group">
|
||||
<h2 class="title">{subcategory.Name}</h2>
|
||||
<p>В категогрии {subcategory.Name} собраны {strconv.Itoa(subcategory.Count)} курсов. Раз в неделю мы обновляем информацию о всех курсах.</p>
|
||||
|
||||
<h2 class="title">{ subcategory.Name }</h2>
|
||||
<p>В категогрии { subcategory.Name } собраны { strconv.Itoa(subcategory.Count) } курсов. Раз в неделю мы обновляем информацию о всех курсах.</p>
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
for _, info := range subcategory.Courses {
|
||||
@listCoursesCard(info)
|
||||
@ -147,30 +139,29 @@ css myImg() {
|
||||
min-width: 19rem;
|
||||
}
|
||||
|
||||
|
||||
css cardTextSize() {
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
templ listCoursesCard(info CourseInfo) {
|
||||
// <div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128")} alt="Course picture" class={"card-img-top"}/>
|
||||
<div class={"card-body", cardTextSize(), "row"}>
|
||||
<h5 class="card-title">{info.Name}</h5>
|
||||
<div class="input-group d-flex align-self-end">
|
||||
<a
|
||||
href={ templ.URL(info.OriginLink) }
|
||||
class="btn text btn-outline-primary flex-grow-1"
|
||||
>Go!</a>
|
||||
<span class="input-group-text justify-content-end flex-fill">
|
||||
{strconv.Itoa(info.FullPrice)} rub.
|
||||
</span>
|
||||
</div>
|
||||
// <div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128") } alt="Course picture" class={ "card-img-top" }/>
|
||||
<div class={ "card-body", cardTextSize(), "row" }>
|
||||
<h5 class="card-title">{ info.Name }</h5>
|
||||
<div class="input-group d-flex align-self-end">
|
||||
<a
|
||||
href={ templ.URL(info.OriginLink) }
|
||||
class="btn text btn-outline-primary flex-grow-1"
|
||||
>Go!</a>
|
||||
<span class="input-group-text justify-content-end flex-fill">
|
||||
{ strconv.Itoa(info.FullPrice) } rub.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ListCourses(pageType PageKind, s stats, params ListCoursesParams) {
|
||||
|
||||
@ -16,27 +16,27 @@ import "strconv"
|
||||
|
||||
func breadcrumbsLoad() templ.ComponentScript {
|
||||
return templ.ComponentScript{
|
||||
Name: `__templ_breadcrumbsLoad_9a1d`,
|
||||
Function: `function __templ_breadcrumbsLoad_9a1d(){const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
Name: `__templ_breadcrumbsLoad_e656`,
|
||||
Function: `function __templ_breadcrumbsLoad_e656(){const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
const lt = document.getElementById('learning-type-filter');
|
||||
const ct = document.getElementById('course-thematic-filter');
|
||||
const lt = document.getElementById('learning-type-filter');
|
||||
const ct = document.getElementById('course-thematic-filter');
|
||||
|
||||
const prefix = (lt !== null && lt.value !== '') ? ` + "`" + `/courses/${lt.value}` + "`" + ` : ` + "`" + `/courses` + "`" + `;
|
||||
const out = (ct !== null && ct.value !== '') ? ` + "`" + `${prefix}/${ct.value}` + "`" + ` : prefix;
|
||||
const prefix = (lt !== null && lt.value !== '') ? ` + "`" + `/courses/${lt.value}` + "`" + ` : ` + "`" + `/courses` + "`" + `;
|
||||
const out = (ct !== null && ct.value !== '') ? ` + "`" + `${prefix}/${ct.value}` + "`" + ` : prefix;
|
||||
|
||||
document.location.assign(out);
|
||||
return false;
|
||||
};
|
||||
document.location.assign(out);
|
||||
return false;
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ff = document.getElementById('filter-form');
|
||||
if (ff === null) return;
|
||||
ff.addEventListener('submit', formFilterOnSubmit);
|
||||
});}`,
|
||||
Call: templ.SafeScript(`__templ_breadcrumbsLoad_9a1d`),
|
||||
CallInline: templ.SafeScriptInline(`__templ_breadcrumbsLoad_9a1d`),
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ff = document.getElementById('filter-form');
|
||||
if (ff === null) return;
|
||||
ff.addEventListener('submit', formFilterOnSubmit);
|
||||
});}`,
|
||||
Call: templ.SafeScript(`__templ_breadcrumbsLoad_e656`),
|
||||
CallInline: templ.SafeScriptInline(`__templ_breadcrumbsLoad_e656`),
|
||||
}
|
||||
}
|
||||
|
||||
@ -322,7 +322,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(learningType.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 96, Col: 25}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 92, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -399,7 +399,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 109, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 104, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -451,7 +451,7 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(container.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 121, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 116, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -499,7 +499,7 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 133, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 126, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -517,7 +517,7 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 134, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 127, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -535,7 +535,7 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(subcategory.Count))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 134, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 127, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -654,7 +654,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 160, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 151, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -685,7 +685,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 167, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 158, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
58
internal/kurious/ports/http/bootstrap/main.templ
Normal file
58
internal/kurious/ports/http/bootstrap/main.templ
Normal file
@ -0,0 +1,58 @@
|
||||
package bootstrap
|
||||
|
||||
import "strconv"
|
||||
|
||||
type IndexCourseCategoryItem struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Count int
|
||||
}
|
||||
|
||||
// courseItemCard is a card that renders a single course thematic item
|
||||
// that holds multiple learning types. It expected to have a basic description
|
||||
// and an amount of items.
|
||||
templ courseItemCard(item IndexCourseCategoryItem) {
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{ item.Name }</h5>
|
||||
<hr/>
|
||||
<p>{ item.Description }</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a
|
||||
href={ templ.URL("/courses/" + item.ID) }
|
||||
class="btn btn-sm btn-outline-primary col-6"
|
||||
>
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">
|
||||
{ strconv.Itoa(item.Count) } items.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ courseCategory(items []IndexCourseCategoryItem) {
|
||||
<div class="container w-75">
|
||||
<div class="row g-4">
|
||||
for _, item := range items {
|
||||
<div class="col-12 col-md-8 col-lg-4">
|
||||
@courseItemCard(item)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
type MainPageParams struct{
|
||||
Breadcrumbs BreadcrumbsParams
|
||||
Categories []IndexCourseCategoryItem
|
||||
}
|
||||
|
||||
templ MainPage(pageType PageKind, s stats, params MainPageParams) {
|
||||
@root(pageType, s) {
|
||||
@listCoursesSectionHeader(params.Breadcrumbs)
|
||||
@courseCategory(params.Categories)
|
||||
}
|
||||
}
|
||||
207
internal/kurious/ports/http/bootstrap/main_templ.go
Normal file
207
internal/kurious/ports/http/bootstrap/main_templ.go
Normal file
@ -0,0 +1,207 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.513
|
||||
package bootstrap
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
import "strconv"
|
||||
|
||||
type IndexCourseCategoryItem struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Count int
|
||||
}
|
||||
|
||||
// courseItemCard is a card that renders a single course thematic item
|
||||
// that holds multiple learning types. It expected to have a basic description
|
||||
// and an amount of items.
|
||||
func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"card\"><div class=\"card-body\"><h5 class=\"card-title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 17, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h5><hr><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 19, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"d-flex justify-content-between align-items-center\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.SafeURL = templ.URL("/courses/" + item.ID)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"btn btn-sm btn-outline-primary col-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var5 := `Open`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> <small class=\"text-body-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(item.Count))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 28, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var7 := `items.`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</small></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func courseCategory(items []IndexCourseCategoryItem) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var8 == nil {
|
||||
templ_7745c5c3_Var8 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container w-75\"><div class=\"row g-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range items {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-12 col-md-8 col-lg-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = courseItemCard(item).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
type MainPageParams struct {
|
||||
Breadcrumbs BreadcrumbsParams
|
||||
Categories []IndexCourseCategoryItem
|
||||
}
|
||||
|
||||
func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var9 == nil {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var10 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
templ_7745c5c3_Err = listCoursesSectionHeader(params.Breadcrumbs).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = courseCategory(params.Categories).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
@ -2,352 +2,267 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/command"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/ports/http/bootstrap"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type courseServer struct {
|
||||
var (
|
||||
paramsAttr = attribute.Key("params")
|
||||
|
||||
webtracer = otel.Tracer("http")
|
||||
)
|
||||
|
||||
type courseTemplServer struct {
|
||||
app service.Application
|
||||
log *slog.Logger
|
||||
|
||||
useTailwind bool
|
||||
}
|
||||
|
||||
type pagination struct {
|
||||
nextPageToken string
|
||||
perPage int
|
||||
}
|
||||
|
||||
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
||||
query := r.URL.Query()
|
||||
out.nextPageToken = query.Get("next")
|
||||
|
||||
if query.Has("per_page") {
|
||||
out.perPage, err = strconv.Atoi(query.Get("per_page"))
|
||||
if err != nil {
|
||||
return out, errors.NewValidationError("per_page", "bad per_page value")
|
||||
}
|
||||
} else {
|
||||
out.perPage = 50
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
const (
|
||||
LearningTypePathParam = "learning_type"
|
||||
ThematicTypePathParam = "thematic_type"
|
||||
)
|
||||
|
||||
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
||||
out.pagination, err = parsePaginationFromQuery(r)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
out.learningType = vars[LearningTypePathParam]
|
||||
out.courseThematic = vars[ThematicTypePathParam]
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type listCoursesParams struct {
|
||||
pagination
|
||||
|
||||
courseThematic string
|
||||
learningType string
|
||||
}
|
||||
|
||||
type baseInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
type categoryInfo struct {
|
||||
baseInfo
|
||||
|
||||
Subcategories []subcategoryInfo
|
||||
}
|
||||
|
||||
type subcategoryInfo struct {
|
||||
baseInfo
|
||||
|
||||
Courses []domain.Course
|
||||
}
|
||||
|
||||
type IDNamePair struct {
|
||||
ID string
|
||||
Name string
|
||||
IsActive bool
|
||||
}
|
||||
|
||||
type listCoursesTemplateParams struct {
|
||||
Categories []categoryInfo
|
||||
NextPageToken string
|
||||
AvailableLearningTypes []IDNamePair
|
||||
AvailableCourseThematics []IDNamePair
|
||||
|
||||
ActiveLearningType string
|
||||
LearningTypeName string
|
||||
|
||||
ActiveCourseThematic string
|
||||
CourseThematicName string
|
||||
}
|
||||
|
||||
func mapDomainCourseToTemplate(in ...domain.Course) listCoursesTemplateParams {
|
||||
coursesBySubcategory := make(map[string][]domain.Course, len(in))
|
||||
func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) bootstrap.ListCoursesParams {
|
||||
coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in))
|
||||
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
||||
categoryByID := make(map[string]baseInfo, len(in))
|
||||
categoryByID := make(map[string]bootstrap.CategoryBaseInfo, len(in))
|
||||
|
||||
xslices.ForEach(in, func(c domain.Course) {
|
||||
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], c)
|
||||
courseInfo := bootstrap.CourseInfo{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
FullPrice: int(c.FullPrice),
|
||||
ImageLink: c.ImageLink,
|
||||
OriginLink: c.OriginLink,
|
||||
}
|
||||
|
||||
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], courseInfo)
|
||||
|
||||
if _, ok := subcategoriesByCategories[c.LearningTypeID]; !ok {
|
||||
subcategoriesByCategories[c.LearningTypeID] = map[string]struct{}{}
|
||||
}
|
||||
subcategoriesByCategories[c.LearningTypeID][c.ThematicID] = struct{}{}
|
||||
|
||||
if _, ok := categoryByID[c.LearningTypeID]; !ok {
|
||||
categoryByID[c.LearningTypeID] = baseInfo{
|
||||
categoryByID[c.LearningTypeID] = bootstrap.CategoryBaseInfo{
|
||||
ID: c.LearningTypeID,
|
||||
Name: c.LearningType,
|
||||
}
|
||||
}
|
||||
if _, ok := categoryByID[c.ThematicID]; !ok {
|
||||
categoryByID[c.ThematicID] = baseInfo{
|
||||
ID: c.ThematicID,
|
||||
Name: c.Thematic,
|
||||
categoryByID[c.ThematicID] = bootstrap.CategoryBaseInfo{
|
||||
ID: c.ThematicID,
|
||||
Name: c.Thematic,
|
||||
Count: counts[c.ThematicID],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var out listCoursesTemplateParams
|
||||
for category, subcategoryMap := range subcategoriesByCategories {
|
||||
outCategory := categoryInfo{
|
||||
baseInfo: categoryByID[category],
|
||||
var out bootstrap.ListCoursesParams
|
||||
for categoryID, subcategoriesID := range subcategoriesByCategories {
|
||||
outCategory := bootstrap.CategoryContainer{
|
||||
CategoryBaseInfo: categoryByID[categoryID],
|
||||
}
|
||||
|
||||
for subcategory := range subcategoryMap {
|
||||
outSubCategory := subcategoryInfo{
|
||||
baseInfo: categoryByID[subcategory],
|
||||
Courses: coursesBySubcategory[subcategory],
|
||||
for subcategoryID := range subcategoriesID {
|
||||
outSubcategory := bootstrap.SubcategoryContainer{
|
||||
CategoryBaseInfo: categoryByID[subcategoryID],
|
||||
Courses: coursesBySubcategory[subcategoryID],
|
||||
}
|
||||
|
||||
outCategory.Subcategories = append(outCategory.Subcategories, outSubCategory)
|
||||
outCategory.Subcategories = append(outCategory.Subcategories, outSubcategory)
|
||||
}
|
||||
sort.Slice(outCategory.Subcategories, func(i, j int) bool {
|
||||
return outCategory.Subcategories[i].ID < outCategory.Subcategories[j].ID
|
||||
})
|
||||
|
||||
out.Categories = append(out.Categories, outCategory)
|
||||
}
|
||||
sort.Slice(out.Categories, func(i, j int) bool {
|
||||
return out.Categories[i].ID < out.Categories[j].ID
|
||||
})
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
params, err := parseListCoursesParams(r)
|
||||
var span trace.Span
|
||||
ctx, span = webtracer.Start(ctx, "list")
|
||||
defer func() {
|
||||
span.End()
|
||||
}()
|
||||
|
||||
stats := bootstrap.MakeNewStats(10_240, 2_560_000, 1800)
|
||||
|
||||
pathParams, err := parseListCoursesParams(r)
|
||||
if handleError(ctx, err, w, c.log, "unable to parse list courses params") {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||
CourseThematic: params.courseThematic,
|
||||
LearningType: params.learningType,
|
||||
Limit: params.perPage,
|
||||
NextPageToken: params.nextPageToken,
|
||||
jsonParams, _ := json.Marshal(pathParams)
|
||||
span.SetAttributes(paramsAttr.String(string(jsonParams)))
|
||||
|
||||
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||
CourseThematic: pathParams.CourseThematic,
|
||||
LearningType: pathParams.LearningType,
|
||||
Limit: pathParams.PerPage,
|
||||
NextPageToken: pathParams.NextPageToken,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
courses := result.Courses
|
||||
templateCourses := mapDomainCourseToTemplate(courses...)
|
||||
templateCourses.NextPageToken = result.NextPageToken
|
||||
params := makeTemplListCoursesParams(listCoursesResult.AvailableCoursesOfSub, listCoursesResult.Courses...)
|
||||
|
||||
learningTypeList, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||
return
|
||||
}
|
||||
|
||||
templateCourses.AvailableLearningTypes = xslices.Map(learningTypeList.LearningTypes, func(in query.LearningType) IDNamePair {
|
||||
if in.ID == params.learningType {
|
||||
templateCourses.LearningTypeName = in.Name
|
||||
params.FilterForm.AvailableLearningTypes = xslices.Map(learningTypeResult.LearningTypes, func(in query.LearningType) bootstrap.Category {
|
||||
outcategory := bootstrap.Category{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
}
|
||||
return IDNamePair{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
IsActive: in.ID == params.learningType,
|
||||
if in.ID == pathParams.LearningType {
|
||||
params.FilterForm.ActiveLearningType = outcategory
|
||||
}
|
||||
|
||||
return outcategory
|
||||
})
|
||||
|
||||
templateCourses.ActiveLearningType = params.learningType
|
||||
templateCourses.ActiveCourseThematic = params.courseThematic
|
||||
|
||||
if params.learningType != "" {
|
||||
if pathParams.LearningType != "" {
|
||||
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
|
||||
LearningTypeID: params.learningType,
|
||||
LearningTypeID: pathParams.LearningType,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
|
||||
return
|
||||
}
|
||||
|
||||
templateCourses.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) IDNamePair {
|
||||
if in.ID == params.courseThematic {
|
||||
templateCourses.CourseThematicName = in.Name
|
||||
params.FilterForm.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) bootstrap.Category {
|
||||
outcategory := bootstrap.Category{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
}
|
||||
return IDNamePair{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
IsActive: in.ID == params.courseThematic,
|
||||
if pathParams.CourseThematic == in.ID {
|
||||
params.FilterForm.BreadcrumbsParams.ActiveCourseThematic = outcategory
|
||||
}
|
||||
|
||||
return outcategory
|
||||
})
|
||||
}
|
||||
|
||||
var tmpl *template.Template
|
||||
if c.useTailwind {
|
||||
tmpl = getTemplateHTMLBySpecificFiles(ctx, c.log, "list.html")
|
||||
} else {
|
||||
tmpl = getCoreTemplate(ctx, c.log)
|
||||
c.log.DebugContext(
|
||||
ctx, "using bootstrap",
|
||||
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
|
||||
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
|
||||
)
|
||||
|
||||
params = bootstrap.ListCoursesParams{
|
||||
FilterForm: bootstrap.FilterFormParams{
|
||||
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
|
||||
ActiveLearningType: params.FilterForm.ActiveLearningType,
|
||||
ActiveCourseThematic: params.FilterForm.ActiveCourseThematic,
|
||||
},
|
||||
AvailableLearningTypes: params.FilterForm.AvailableLearningTypes,
|
||||
AvailableCourseThematics: params.FilterForm.AvailableCourseThematics,
|
||||
},
|
||||
Categories: params.Categories,
|
||||
}
|
||||
err = tmpl.ExecuteTemplate(w, "courses", templateCourses)
|
||||
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||
|
||||
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.CategoryContainer) int {
|
||||
if lhs.Count > rhs.Count {
|
||||
return 1
|
||||
} else if lhs.Count < rhs.Count {
|
||||
return -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
span.AddEvent("starting to render")
|
||||
err = bootstrap.ListCourses(bootstrap.PageCourses, stats, params).Render(ctx, w)
|
||||
span.AddEvent("render finished")
|
||||
|
||||
if handleError(ctx, err, w, c.log, "unable to render list courses") {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c courseServer) Get(w http.ResponseWriter, r *http.Request) {
|
||||
func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
id := mux.Vars(r)["course_id"]
|
||||
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
||||
ID: id,
|
||||
var span trace.Span
|
||||
ctx, span = webtracer.Start(ctx, "index")
|
||||
defer func() {
|
||||
span.End()
|
||||
}()
|
||||
|
||||
stats := bootstrap.MakeNewStats(1, 2, 3)
|
||||
|
||||
coursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{})
|
||||
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
params := bootstrap.MainPageParams{
|
||||
Categories: []bootstrap.IndexCourseCategoryItem{},
|
||||
}
|
||||
|
||||
coursesByLearningType := make(map[IDNamePair][]domain.Course)
|
||||
xslices.ForEach(coursesResult.Courses, func(in domain.Course) {
|
||||
pair := IDNamePair{
|
||||
ID: in.LearningTypeID,
|
||||
Name: in.LearningType,
|
||||
}
|
||||
coursesByLearningType[pair] = append(coursesByLearningType[pair], in)
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to get course") {
|
||||
return
|
||||
|
||||
for learningTypeInfo, courses := range coursesByLearningType {
|
||||
category := bootstrap.IndexCourseCategoryItem{
|
||||
ID: learningTypeInfo.ID,
|
||||
Name: learningTypeInfo.Name,
|
||||
Count: len(courses),
|
||||
}
|
||||
|
||||
xslices.Shuffle(courses)
|
||||
if len(courses) > 3 {
|
||||
courses = courses[:3]
|
||||
}
|
||||
|
||||
names := xslices.Map(courses, func(in domain.Course) string {
|
||||
return in.Name
|
||||
})
|
||||
|
||||
namesStr := strings.Join(names, ",")
|
||||
|
||||
category.Description = fmt.Sprintf(
|
||||
"Here you can find courses"+
|
||||
" such as %s",
|
||||
namesStr,
|
||||
)
|
||||
|
||||
params.Categories = append(params.Categories, category)
|
||||
}
|
||||
|
||||
payload, err := json.MarshalIndent(course, "", " ")
|
||||
if handleError(ctx, err, w, c.log, "unable to marshal json") {
|
||||
return
|
||||
}
|
||||
w.Header().Set("content-type", "application/json")
|
||||
w.Header().Set("content-length", strconv.Itoa(len(payload)))
|
||||
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.IndexCourseCategoryItem) int {
|
||||
if lhs.Count < rhs.Count {
|
||||
return 1
|
||||
} else if lhs.Count > rhs.Count {
|
||||
return -1
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(payload))
|
||||
if err != nil {
|
||||
xcontext.LogWithWarnError(ctx, c.log, err, "unable to write a message")
|
||||
}
|
||||
}
|
||||
|
||||
func (c courseServer) GetShort(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
id := mux.Vars(r)["course_id"]
|
||||
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
||||
ID: id,
|
||||
return 0
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to get course") {
|
||||
return
|
||||
}
|
||||
|
||||
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "course_info", course)
|
||||
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||
span.AddEvent("starting to render")
|
||||
err = bootstrap.MainPage(bootstrap.PageIndex, stats, params).Render(ctx, w)
|
||||
span.AddEvent("render finished")
|
||||
if handleError(ctx, err, w, c.log, "rendeting template") {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c courseServer) RenderEditDescription(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
id := mux.Vars(r)["course_id"]
|
||||
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
||||
ID: id,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to get course") {
|
||||
return
|
||||
}
|
||||
|
||||
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "edit_description", course)
|
||||
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c courseServer) UpdateCourseDescription(w http.ResponseWriter, r *http.Request) {
|
||||
type requestModel struct {
|
||||
ID string `json:"-"`
|
||||
Text string `json:"description"`
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
var req requestModel
|
||||
req.ID = mux.Vars(r)["course_id"]
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if handleError(ctx, err, w, c.log, "unable to read body") {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.app.Commands.UpdateCourseDescription.Handle(ctx, command.UpdateCourseDescription{
|
||||
ID: req.ID,
|
||||
Description: req.Text,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to update course description") {
|
||||
return
|
||||
}
|
||||
|
||||
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
||||
ID: req.ID,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to get course") {
|
||||
return
|
||||
}
|
||||
|
||||
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "course_info", course)
|
||||
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c courseServer) UdpateDescription(w http.ResponseWriter, r *http.Request) {
|
||||
type requestModel struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
var req requestModel
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if handleError(ctx, err, w, c.log, "unable to read body") {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.app.Commands.UpdateCourseDescription.Handle(ctx, command.UpdateCourseDescription{
|
||||
ID: req.ID,
|
||||
Description: req.Text,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to update course description") {
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
@ -1,184 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/ports/http/bootstrap"
|
||||
xtempl "git.loyso.art/frx/kurious/internal/kurious/ports/http/templ"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||
)
|
||||
|
||||
type courseTemplServer struct {
|
||||
app service.Application
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) xtempl.ListCoursesParams {
|
||||
coursesBySubcategory := make(map[string][]xtempl.CourseInfo, len(in))
|
||||
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
||||
categoryByID := make(map[string]xtempl.CategoryBaseInfo, len(in))
|
||||
|
||||
xslices.ForEach(in, func(c domain.Course) {
|
||||
courseInfo := xtempl.CourseInfo{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
FullPrice: int(c.FullPrice),
|
||||
ImageLink: c.ImageLink,
|
||||
OriginLink: c.OriginLink,
|
||||
}
|
||||
|
||||
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], courseInfo)
|
||||
|
||||
if _, ok := subcategoriesByCategories[c.LearningTypeID]; !ok {
|
||||
subcategoriesByCategories[c.LearningTypeID] = map[string]struct{}{}
|
||||
}
|
||||
subcategoriesByCategories[c.LearningTypeID][c.ThematicID] = struct{}{}
|
||||
|
||||
if _, ok := categoryByID[c.LearningTypeID]; !ok {
|
||||
categoryByID[c.LearningTypeID] = xtempl.CategoryBaseInfo{
|
||||
ID: c.LearningTypeID,
|
||||
Name: c.LearningType,
|
||||
}
|
||||
}
|
||||
if _, ok := categoryByID[c.ThematicID]; !ok {
|
||||
categoryByID[c.ThematicID] = xtempl.CategoryBaseInfo{
|
||||
ID: c.ThematicID,
|
||||
Name: c.Thematic,
|
||||
Count: counts[c.ThematicID],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var out xtempl.ListCoursesParams
|
||||
for categoryID, subcategoriesID := range subcategoriesByCategories {
|
||||
outCategory := xtempl.CategoryContainer{
|
||||
CategoryBaseInfo: categoryByID[categoryID],
|
||||
}
|
||||
|
||||
for subcategoryID := range subcategoriesID {
|
||||
outSubcategory := xtempl.SubcategoryContainer{
|
||||
CategoryBaseInfo: categoryByID[subcategoryID],
|
||||
Courses: coursesBySubcategory[subcategoryID],
|
||||
}
|
||||
|
||||
outCategory.Subcategories = append(outCategory.Subcategories, outSubcategory)
|
||||
}
|
||||
|
||||
out.Categories = append(out.Categories, outCategory)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
const useBootstrap = true
|
||||
|
||||
func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
stats := xtempl.MakeNewStats(10_240, 2_560_000, 1800)
|
||||
|
||||
pathParams, err := parseListCoursesParams(r)
|
||||
if handleError(ctx, err, w, c.log, "unable to parse list courses params") {
|
||||
return
|
||||
}
|
||||
|
||||
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||
CourseThematic: pathParams.courseThematic,
|
||||
LearningType: pathParams.learningType,
|
||||
Limit: pathParams.perPage,
|
||||
NextPageToken: pathParams.nextPageToken,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
params := makeTemplListCoursesParams(listCoursesResult.AvailableCoursesOfSub, listCoursesResult.Courses...)
|
||||
|
||||
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||
return
|
||||
}
|
||||
|
||||
params.FilterForm.AvailableLearningTypes = xslices.Map(learningTypeResult.LearningTypes, func(in query.LearningType) xtempl.Category {
|
||||
outcategory := xtempl.Category{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
}
|
||||
if in.ID == pathParams.learningType {
|
||||
params.FilterForm.ActiveLearningType = outcategory
|
||||
}
|
||||
|
||||
return outcategory
|
||||
})
|
||||
|
||||
if pathParams.learningType != "" {
|
||||
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
|
||||
LearningTypeID: pathParams.learningType,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
|
||||
return
|
||||
}
|
||||
|
||||
params.FilterForm.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) xtempl.Category {
|
||||
outcategory := xtempl.Category{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
}
|
||||
if pathParams.courseThematic == in.ID {
|
||||
params.FilterForm.BreadcrumbsParams.ActiveCourseThematic = outcategory
|
||||
}
|
||||
|
||||
return outcategory
|
||||
})
|
||||
}
|
||||
|
||||
if useBootstrap {
|
||||
c.log.DebugContext(
|
||||
ctx, "using bootstrap",
|
||||
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
|
||||
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
|
||||
)
|
||||
|
||||
mapCategory := func(in xtempl.Category) bootstrap.Category {
|
||||
return bootstrap.Category(in)
|
||||
}
|
||||
mapCourseInfo := func(in xtempl.CourseInfo) bootstrap.CourseInfo {
|
||||
return bootstrap.CourseInfo(in)
|
||||
}
|
||||
mapSubcategoryContainer := func(in xtempl.SubcategoryContainer) bootstrap.SubcategoryContainer {
|
||||
return bootstrap.SubcategoryContainer{
|
||||
CategoryBaseInfo: bootstrap.CategoryBaseInfo(in.CategoryBaseInfo),
|
||||
Courses: xslices.Map(in.Courses, mapCourseInfo),
|
||||
}
|
||||
}
|
||||
|
||||
mapCategoryContainer := func(in xtempl.CategoryContainer) bootstrap.CategoryContainer {
|
||||
return bootstrap.CategoryContainer{
|
||||
CategoryBaseInfo: bootstrap.CategoryBaseInfo(in.CategoryBaseInfo),
|
||||
Subcategories: xslices.Map(in.Subcategories, mapSubcategoryContainer),
|
||||
}
|
||||
}
|
||||
stats := bootstrap.MakeNewStats(0, 0, 0)
|
||||
params := bootstrap.ListCoursesParams{
|
||||
FilterForm: bootstrap.FilterFormParams{
|
||||
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
|
||||
ActiveLearningType: bootstrap.Category(params.FilterForm.ActiveLearningType),
|
||||
ActiveCourseThematic: bootstrap.Category(params.FilterForm.ActiveCourseThematic),
|
||||
},
|
||||
AvailableLearningTypes: xslices.Map(params.FilterForm.AvailableLearningTypes, mapCategory),
|
||||
AvailableCourseThematics: xslices.Map(params.FilterForm.AvailableCourseThematics, mapCategory),
|
||||
},
|
||||
Categories: xslices.Map(params.Categories, mapCategoryContainer),
|
||||
}
|
||||
err = bootstrap.ListCourses(bootstrap.PageCourses, stats, params).Render(ctx, w)
|
||||
} else {
|
||||
err = xtempl.ListCourses(stats, params).Render(ctx, w)
|
||||
}
|
||||
if handleError(ctx, err, w, c.log, "unable to render list courses") {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{{ define "base" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{{ template "htmlhead" . }}
|
||||
</head>
|
||||
<body>
|
||||
{{ template "htmlbody" . }}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
|
||||
{{ define "htmlhead" }}
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ .AppName }}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
{{ end }}
|
||||
@ -1,14 +0,0 @@
|
||||
{{ define "htmlbody" }}
|
||||
{{ template "header" .}}
|
||||
{{ template "body" .}}
|
||||
{{ template "footer" .}}
|
||||
{{ end }}
|
||||
|
||||
{{ define "header" }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "body" }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "footer" }}
|
||||
{{ end }}
|
||||
@ -1,67 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
)
|
||||
|
||||
const (
|
||||
baseTemplatePath = "./internal/kurious/ports/http"
|
||||
templateDir = "/templates"
|
||||
htmlPath = "/html"
|
||||
)
|
||||
|
||||
func must[T any](t T, err error) T {
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func scanFiles(dir string) []string {
|
||||
dst := path.Join(baseTemplatePath, dir)
|
||||
entries := xslices.Map(
|
||||
must(os.ReadDir(dst)),
|
||||
func(v fs.DirEntry) string {
|
||||
return path.Join(baseTemplatePath, v.Name())
|
||||
},
|
||||
)
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func getTemplateHTMLBySpecificFiles(ctx context.Context, log *slog.Logger, filenames ...string) *template.Template {
|
||||
filenames = append([]string{"index.html"}, filenames...)
|
||||
dir := path.Join(baseTemplatePath, htmlPath)
|
||||
out := xslices.Map(filenames, func(in string) string {
|
||||
return path.Join(dir, in)
|
||||
})
|
||||
|
||||
tmpl, err := template.New("courses").ParseFiles(out...)
|
||||
if err != nil {
|
||||
xcontext.LogWithWarnError(ctx, log, err, "unable to parse template")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return tmpl
|
||||
}
|
||||
|
||||
func getCoreTemplate(ctx context.Context, log *slog.Logger) *template.Template {
|
||||
filenames := scanFiles(templateDir)
|
||||
out, err := template.New("courses").ParseFiles(filenames...)
|
||||
if err != nil {
|
||||
xcontext.LogWithWarnError(ctx, log, err, "unable to parse template")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@ -5,10 +5,15 @@ import (
|
||||
stderrors "errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
@ -23,15 +28,7 @@ func NewServer(app service.Application, log *slog.Logger) Server {
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) Courses(useTailwind bool) courseServer {
|
||||
return courseServer{
|
||||
app: s.app,
|
||||
log: s.log,
|
||||
useTailwind: useTailwind,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) CoursesByTempl() courseTemplServer {
|
||||
func (s Server) Courses() courseTemplServer {
|
||||
return courseTemplServer(s)
|
||||
}
|
||||
|
||||
@ -40,6 +37,10 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
|
||||
return false
|
||||
}
|
||||
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, "error during handling request")
|
||||
span.RecordError(err)
|
||||
|
||||
var errorString string
|
||||
var code int
|
||||
valErr := new(errors.ValidationError)
|
||||
@ -61,3 +62,55 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type pagination struct {
|
||||
NextPageToken string
|
||||
PerPage int
|
||||
}
|
||||
|
||||
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
||||
query := r.URL.Query()
|
||||
out.NextPageToken = query.Get("next")
|
||||
|
||||
if query.Has("per_page") {
|
||||
out.PerPage, err = strconv.Atoi(query.Get("per_page"))
|
||||
if err != nil {
|
||||
return out, errors.NewValidationError("per_page", "bad per_page value")
|
||||
}
|
||||
} else {
|
||||
out.PerPage = 50
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
const (
|
||||
LearningTypePathParam = "learning_type"
|
||||
ThematicTypePathParam = "thematic_type"
|
||||
)
|
||||
|
||||
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
||||
out.pagination, err = parsePaginationFromQuery(r)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
out.LearningType = vars[LearningTypePathParam]
|
||||
out.CourseThematic = vars[ThematicTypePathParam]
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type listCoursesParams struct {
|
||||
pagination
|
||||
|
||||
CourseThematic string
|
||||
LearningType string
|
||||
}
|
||||
|
||||
type IDNamePair struct {
|
||||
ID string
|
||||
Name string
|
||||
IsActive bool
|
||||
}
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
package templ
|
||||
|
||||
templ button(title string, attributes templ.Attributes) {
|
||||
<button class="button" { attributes... }>{ title }</button>
|
||||
}
|
||||
|
||||
templ buttonRedirect(id, title string, linkTo string) {
|
||||
<button
|
||||
class="button"
|
||||
id={ "origin-link-" + id }
|
||||
>
|
||||
{ title }
|
||||
</button>
|
||||
|
||||
@onclickRedirect("origin-link-" + id, linkTo)
|
||||
}
|
||||
|
||||
script onclickRedirect(id, to string) {
|
||||
document.getElementById(id).onclick = () => {
|
||||
location.href = to
|
||||
}
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.513
|
||||
package templ
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
func button(title string, attributes templ.Attributes) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"button\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, attributes)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/common.templ`, Line: 3, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func buttonRedirect(id, title string, linkTo string) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"button\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("origin-link-" + id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/common.templ`, Line: 11, Col: 8}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = onclickRedirect("origin-link-"+id, linkTo).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func onclickRedirect(id, to string) templ.ComponentScript {
|
||||
return templ.ComponentScript{
|
||||
Name: `__templ_onclickRedirect_47ae`,
|
||||
Function: `function __templ_onclickRedirect_47ae(id, to){document.getElementById(id).onclick = () => {
|
||||
location.href = to
|
||||
}}`,
|
||||
Call: templ.SafeScript(`__templ_onclickRedirect_47ae`, id, to),
|
||||
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_47ae`, id, to),
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package templ
|
||||
|
||||
templ head() {
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Courses Aggregator</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.8.0"></script>
|
||||
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/>
|
||||
<link rel="manifest" href="/site.webmanifest"/>
|
||||
</head>
|
||||
}
|
||||
|
||||
templ navigation() {
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
Courses
|
||||
</div>
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
<div id="navbarBasicExample" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item">
|
||||
Home
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Find
|
||||
</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
More
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item">
|
||||
About
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Contact
|
||||
</a>
|
||||
<hr class="navbar-divider"/>
|
||||
<a class="navbar-item">
|
||||
Report an issue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
|
||||
templ footer() {
|
||||
<footer>
|
||||
Here will be a footer
|
||||
</footer>
|
||||
}
|
||||
@ -1,182 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.513
|
||||
package templ
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
func head() templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var2 := `Courses Aggregator`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><script src=\"https://unpkg.com/htmx.org@1.8.0\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var3 := ``
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</script><script src=\"https://unpkg.com/htmx.org/dist/ext/json-enc.js\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var4 := ``
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</script><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css\"><link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\"><link rel=\"manifest\" href=\"/site.webmanifest\"></head>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func navigation() templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var5 == nil {
|
||||
templ_7745c5c3_Var5 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"navbar\" role=\"navigation\" aria-label=\"main navigation\"><div class=\"navbar-brand\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var6 := `Courses`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><a role=\"button\" class=\"navbar-burger\" aria-label=\"menu\" aria-expanded=\"false\"><span aria-hidden=\"true\"></span> <span aria-hidden=\"true\"></span> <span aria-hidden=\"true\"></span></a><div id=\"navbarBasicExample\" class=\"navbar-menu\"><div class=\"navbar-start\"><a class=\"navbar-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var7 := `Home`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> <a class=\"navbar-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var8 := `Find`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><div class=\"navbar-item has-dropdown is-hoverable\"><a class=\"navbar-link\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var9 := `More`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><div class=\"navbar-dropdown\"><a class=\"navbar-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var10 := `About`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> <a class=\"navbar-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var11 := `Contact`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><hr class=\"navbar-divider\"><a class=\"navbar-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var12 := `Report an issue`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div></div></div></div></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func footer() templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var13 == nil {
|
||||
templ_7745c5c3_Var13 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var14 := `Here will be a footer`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
@ -1,209 +0,0 @@
|
||||
package templ
|
||||
|
||||
import "strconv"
|
||||
import "fmt"
|
||||
|
||||
script breadcrumbsLoad() {
|
||||
const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
const lt = document.getElementById('learning-type-filter');
|
||||
const ct = document.getElementById('course-thematic-filter');
|
||||
|
||||
const prefix = (lt !== null && lt.value !== '') ? `/courses/${lt.value}` : `/courses`;
|
||||
const out = (ct !== null && ct.value !== '') ? `${prefix}/${ct.value}` : prefix;
|
||||
|
||||
document.location.assign(out);
|
||||
return false;
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ff = document.getElementById('filter-form');
|
||||
if (ff === null) return;
|
||||
ff.addEventListener('submit', formFilterOnSubmit);
|
||||
});
|
||||
}
|
||||
|
||||
templ breadcrumbItem(enabled bool, link string, isLink bool, title string) {
|
||||
if enabled {
|
||||
<li>
|
||||
<a
|
||||
if !isEmpty(link) {
|
||||
href={ templ.SafeURL("/courses" + link) }
|
||||
itemprop="url"
|
||||
}
|
||||
>
|
||||
<span
|
||||
itemprop="title"
|
||||
if isEmpty(link) {
|
||||
itemprop="url"
|
||||
}
|
||||
>
|
||||
{ title }
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
templ listCourseHeader(params FilterFormParams) {
|
||||
<div class="container block">
|
||||
@breadcrumb(params.BreadcrumbsParams)
|
||||
@filterForm(params)
|
||||
</div>
|
||||
}
|
||||
|
||||
templ breadcrumb(params BreadcrumbsParams) {
|
||||
<nav
|
||||
class="breadcrumb"
|
||||
aria-label="breadcrumbs"
|
||||
itemprop="breadcrumb"
|
||||
itemtype="https://schema.org/BreadcrumbList"
|
||||
itemscope
|
||||
>
|
||||
<ul>
|
||||
@breadcrumbItem(true, "/courses", !isEmpty(params.ActiveLearningType.ID), "Курсы")
|
||||
@breadcrumbItem(!params.ActiveLearningType.Empty(), "/" + params.ActiveLearningType.ID, !params.ActiveCourseThematic.Empty(), params.ActiveLearningType.Name)
|
||||
@breadcrumbItem(!params.ActiveCourseThematic.Empty(), "", false, params.ActiveCourseThematic.Name)
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
templ filterForm(params FilterFormParams) {
|
||||
<form id="filter-form" class="columns">
|
||||
<div class="select">
|
||||
<select id="learning-type-filter" name="learning_type">
|
||||
<option value="">All learnings</option>
|
||||
for _, item := range params.AvailableLearningTypes {
|
||||
<option
|
||||
value={ item.ID }
|
||||
if item.ID == params.ActiveLearningType.ID {
|
||||
selected
|
||||
}
|
||||
>
|
||||
{ item.Name }
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
if !params.ActiveLearningType.Empty() {
|
||||
<div class="select">
|
||||
<select id="course-thematic-filter" name="course_thematic">
|
||||
<option value="">All course thematics</option>
|
||||
for _, item := range params.AvailableCourseThematics {
|
||||
<option
|
||||
value={ item.ID }
|
||||
if item.ID == params.ActiveCourseThematic.ID {
|
||||
selected
|
||||
}
|
||||
>
|
||||
{ item.Name }
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
@button("goto", templ.Attributes{"id": "go-to-filter"})
|
||||
</form>
|
||||
}
|
||||
|
||||
templ listCoursesContainer(categories []CategoryContainer) {
|
||||
<div id="category-course-list" class="container">
|
||||
for _, category := range categories {
|
||||
<div class="box">
|
||||
<div class="title is-3">
|
||||
<a href={ templ.URL("/courses/" + category.ID) }>{ category.Name }</a>
|
||||
</div>
|
||||
<div class="subtitle is-6">
|
||||
This category contains a lot of interesing courses. Check them out!
|
||||
</div>
|
||||
for _, subcategory := range category.Subcategories {
|
||||
<div class="box">
|
||||
<div class="title is-4">
|
||||
<a href={ templ.URL(fmt.Sprintf("/courses/%s/%s", category.ID, subcategory.ID)) }>
|
||||
{ subcategory.Name }
|
||||
</a>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
for _, course := range subcategory.Courses {
|
||||
@courseInfoElement(course)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ListCourses(s stats, params ListCoursesParams) {
|
||||
@root(s) {
|
||||
@listCourseHeader(params.FilterForm)
|
||||
@listCoursesContainer(params.Categories)
|
||||
|
||||
<div id="course-info"></div>
|
||||
}
|
||||
}
|
||||
|
||||
templ root(s stats) {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@head()
|
||||
<body>
|
||||
@navigation()
|
||||
<nav class="level">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Courses</p>
|
||||
<p class="title">{ s.CoursesCount }</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Clients</p>
|
||||
<p class="title">{ s.ClientsCount }</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Categories</p>
|
||||
<p class="title">{ s.CategoriesCount }</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="section">
|
||||
{ children... }
|
||||
</div>
|
||||
@footer()
|
||||
@breadcrumbsLoad()
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ courseInfoElement(params CourseInfo) {
|
||||
<article class="column is-one-quarter" hx-target="this" hx-swap="outerHTML">
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<figure class="image">
|
||||
<img src={ params.ImageLink }/>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="media-content">
|
||||
<p class="title is-5">{ params.Name }</p>
|
||||
<p class="subtitle is-8">oh well</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
if params.FullPrice > 0 {
|
||||
<p>{ strconv.Itoa(params.FullPrice) } руб.</p>
|
||||
} else {
|
||||
<p>Бесплатно</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@buttonRedirect(params.ID, "Show course", params.OriginLink)
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
@ -1,714 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.513
|
||||
package templ
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
import "strconv"
|
||||
import "fmt"
|
||||
|
||||
func breadcrumbsLoad() templ.ComponentScript {
|
||||
return templ.ComponentScript{
|
||||
Name: `__templ_breadcrumbsLoad_9a1d`,
|
||||
Function: `function __templ_breadcrumbsLoad_9a1d(){const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
const lt = document.getElementById('learning-type-filter');
|
||||
const ct = document.getElementById('course-thematic-filter');
|
||||
|
||||
const prefix = (lt !== null && lt.value !== '') ? ` + "`" + `/courses/${lt.value}` + "`" + ` : ` + "`" + `/courses` + "`" + `;
|
||||
const out = (ct !== null && ct.value !== '') ? ` + "`" + `${prefix}/${ct.value}` + "`" + ` : prefix;
|
||||
|
||||
document.location.assign(out);
|
||||
return false;
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ff = document.getElementById('filter-form');
|
||||
if (ff === null) return;
|
||||
ff.addEventListener('submit', formFilterOnSubmit);
|
||||
});}`,
|
||||
Call: templ.SafeScript(`__templ_breadcrumbsLoad_9a1d`),
|
||||
CallInline: templ.SafeScriptInline(`__templ_breadcrumbsLoad_9a1d`),
|
||||
}
|
||||
}
|
||||
|
||||
func breadcrumbItem(enabled bool, link string, isLink bool, title string) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if enabled {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !isEmpty(link) {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL("/courses" + link)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" itemprop=\"url\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("><span itemprop=\"title\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if isEmpty(link) {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" itemprop=\"url\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 41, Col: 12}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func listCourseHeader(params FilterFormParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container block\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = breadcrumb(params.BreadcrumbsParams).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = filterForm(params).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func breadcrumb(params BreadcrumbsParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var5 == nil {
|
||||
templ_7745c5c3_Var5 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"breadcrumb\" aria-label=\"breadcrumbs\" itemprop=\"breadcrumb\" itemtype=\"https://schema.org/BreadcrumbList\" itemscope><ul>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = breadcrumbItem(true, "/courses", !isEmpty(params.ActiveLearningType.ID), "Курсы").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = breadcrumbItem(!params.ActiveLearningType.Empty(), "/"+params.ActiveLearningType.ID, !params.ActiveCourseThematic.Empty(), params.ActiveLearningType.Name).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = breadcrumbItem(!params.ActiveCourseThematic.Empty(), "", false, params.ActiveCourseThematic.Name).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func filterForm(params FilterFormParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var6 == nil {
|
||||
templ_7745c5c3_Var6 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form id=\"filter-form\" class=\"columns\"><div class=\"select\"><select id=\"learning-type-filter\" name=\"learning_type\"><option value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var7 := `All learnings`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range params.AvailableLearningTypes {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(item.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.ID == params.ActiveLearningType.ID {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 83, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !params.ActiveLearningType.Empty() {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"select\"><select id=\"course-thematic-filter\" name=\"course_thematic\"><option value=\"\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var9 := `All course thematics`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range params.AvailableCourseThematics {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(item.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if item.ID == params.ActiveCourseThematic.ID {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 99, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = button("goto", templ.Attributes{"id": "go-to-filter"}).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func listCoursesContainer(categories []CategoryContainer) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var11 == nil {
|
||||
templ_7745c5c3_Var11 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"category-course-list\" class=\"container\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, category := range categories {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"box\"><div class=\"title is-3\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 templ.SafeURL = templ.URL("/courses/" + category.ID)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var12)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(category.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 114, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div><div class=\"subtitle is-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var14 := `This category contains a lot of interesing courses. Check them out!`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, subcategory := range category.Subcategories {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"box\"><div class=\"title is-4\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 templ.SafeURL = templ.URL(fmt.Sprintf("/courses/%s/%s", category.ID, subcategory.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var15)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 123, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><div class=\"columns is-multiline\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, course := range subcategory.Courses {
|
||||
templ_7745c5c3_Err = courseInfoElement(course).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func ListCourses(s stats, params ListCoursesParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var17 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var17 == nil {
|
||||
templ_7745c5c3_Var17 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var18 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
templ_7745c5c3_Err = listCourseHeader(params.FilterForm).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = listCoursesContainer(params.Categories).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <div id=\"course-info\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
templ_7745c5c3_Err = root(s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var18), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func root(s stats) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var19 == nil {
|
||||
templ_7745c5c3_Var19 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = head().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = navigation().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"level\"><div class=\"level-item has-text-centered\"><div><p class=\"heading\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var20 := `Courses`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(s.CoursesCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 158, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div><div class=\"level-item has-text-centered\"><div><p class=\"heading\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var22 := `Clients`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(s.ClientsCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 164, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div><div class=\"level-item has-text-centered\"><div><p class=\"heading\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var24 := `Categories`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(s.CategoriesCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 170, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div></nav><div class=\"section\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var19.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = footer().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = breadcrumbsLoad().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func courseInfoElement(params CourseInfo) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var26 == nil {
|
||||
templ_7745c5c3_Var26 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<article class=\"column is-one-quarter\" hx-target=\"this\" hx-swap=\"outerHTML\"><div class=\"card\"><div class=\"card-image\"><figure class=\"image\"><img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(params.ImageLink))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"></figure></div><div class=\"card-content\"><div class=\"media-content\"><p class=\"title is-5\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(params.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 193, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"subtitle is-8\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var28 := `oh well`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div><div class=\"content\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if params.FullPrice > 0 {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(params.FullPrice))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 198, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var30 := `руб.`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var31 := `Бесплатно`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = buttonRedirect(params.ID, "Show course", params.OriginLink).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></article>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
package templ
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getCompactedValue(value int) string {
|
||||
var (
|
||||
myValue float64
|
||||
dim string
|
||||
)
|
||||
switch {
|
||||
case value/1e6 > 0:
|
||||
cutted := value / 1e3
|
||||
myValue, dim = float64(cutted)/1e3, "m"
|
||||
case value/1e3 > 0:
|
||||
myValue, dim = float64(value/1e3), "k"
|
||||
default:
|
||||
myValue, dim = float64(value), ""
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(strconv.FormatFloat(myValue, 'f', 3, 32), ".000") + dim
|
||||
}
|
||||
|
||||
func MakeNewStats(courses, clients, categories int) stats {
|
||||
return stats{
|
||||
CoursesCount: getCompactedValue(courses),
|
||||
ClientsCount: getCompactedValue(clients),
|
||||
CategoriesCount: getCompactedValue(categories),
|
||||
}
|
||||
}
|
||||
|
||||
type stats struct {
|
||||
CoursesCount string
|
||||
ClientsCount string
|
||||
CategoriesCount string
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (c Category) Empty() bool {
|
||||
return c == (Category{})
|
||||
}
|
||||
|
||||
type BreadcrumbsParams struct {
|
||||
ActiveLearningType Category
|
||||
ActiveCourseThematic Category
|
||||
}
|
||||
|
||||
type FilterFormParams struct {
|
||||
BreadcrumbsParams
|
||||
|
||||
AvailableLearningTypes []Category
|
||||
AvailableCourseThematics []Category
|
||||
}
|
||||
|
||||
func isEmpty(s string) bool {
|
||||
return s == ""
|
||||
}
|
||||
|
||||
type CourseInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
FullPrice int
|
||||
ImageLink string
|
||||
OriginLink string
|
||||
}
|
||||
|
||||
type CategoryBaseInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Count int
|
||||
}
|
||||
|
||||
type CategoryContainer struct {
|
||||
CategoryBaseInfo
|
||||
|
||||
Subcategories []SubcategoryContainer
|
||||
}
|
||||
|
||||
type SubcategoryContainer struct {
|
||||
CategoryBaseInfo
|
||||
|
||||
Courses []CourseInfo
|
||||
}
|
||||
|
||||
type ListCoursesParams struct {
|
||||
FilterForm FilterFormParams
|
||||
|
||||
Categories []CategoryContainer
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package templ
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetCompactedValue(t *testing.T) {
|
||||
var tt = []struct {
|
||||
name string
|
||||
in int
|
||||
exp string
|
||||
}{
|
||||
{
|
||||
name: "less than 1k",
|
||||
in: 666,
|
||||
exp: "666",
|
||||
},
|
||||
{
|
||||
name: "exactly 1k",
|
||||
in: 1000,
|
||||
exp: "1k",
|
||||
},
|
||||
{
|
||||
name: "some thousands",
|
||||
in: 12345,
|
||||
exp: "12k",
|
||||
},
|
||||
{
|
||||
name: "more thousands",
|
||||
in: 123456,
|
||||
exp: "123k",
|
||||
},
|
||||
{
|
||||
name: "million",
|
||||
in: 1e6,
|
||||
exp: "1m",
|
||||
},
|
||||
{
|
||||
name: "some millions",
|
||||
in: 2e6,
|
||||
exp: "2m",
|
||||
},
|
||||
{
|
||||
name: "more complex value",
|
||||
in: 1.2346e6,
|
||||
exp: "1.234m",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := getCompactedValue(tc.in)
|
||||
if tc.exp != got {
|
||||
t.Errorf("exp=%s got=%s", tc.exp, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
{{ define "html_head" }}
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Courses Aggregator</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.8.0"></script>
|
||||
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
|
||||
</head>
|
||||
{{ end }}
|
||||
|
||||
{{ define "header" }}
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
Courses
|
||||
</div>
|
||||
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
<div id="navbarBasicExample" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item">
|
||||
Home
|
||||
</a>
|
||||
|
||||
<a class="navbar-item">
|
||||
Find
|
||||
</a>
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
More
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item">
|
||||
About
|
||||
</a>
|
||||
<a class="navbar-item">
|
||||
Contact
|
||||
</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item">
|
||||
Report an issue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ define "footer" }}
|
||||
|
||||
<footer>
|
||||
Here will be footer
|
||||
</footer>
|
||||
|
||||
{{ end }}
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
{{ define "course_info" }}
|
||||
<article class="column is-one-quarter" hx-target="this" hx-swap="outerHTML">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-image">
|
||||
<figure class="image">
|
||||
<img src="{{ .ImageLink }}">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="media-content">
|
||||
<p class="title is-5" onclick="location.href='{{ .OriginLink }}'">{{ .Name }}</p>
|
||||
<p class="subtitle is-8">{{ .Description }}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{ if .FullPrice }}
|
||||
<p>{{ .FullPrice }} rub.</p>
|
||||
{{ else }}
|
||||
<p>Бесплатно</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<button class="button" onclick="location.href='{{ .OriginLink }}'">
|
||||
Show Course
|
||||
</button>
|
||||
<button class="button" hx-get="/courses/{{ .ID }}/editdesc">
|
||||
Edit description
|
||||
</button>
|
||||
<!-- <button class="button" hx-get="/courses/{{ .ID }}/" hx-target="#course-info" hx-swap="innerHTML">
|
||||
View course
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
{{ end }}
|
||||
|
||||
{{ define "edit_description" }}
|
||||
<form
|
||||
hx-ext="json-enc"
|
||||
hx-put="/courses/{{ .ID }}/description"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<fieldset disabled>
|
||||
<div class="field">
|
||||
<label class="label">Name</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Text input" value="{{ .Name }}">
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Description</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="description" placeholder="Description">{{ .Description }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Full price: {{ .FullPrice }}</p>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<button class="button is-primary is-link" hx-include="[name='description']">
|
||||
Submit
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-light" hx-get="/courses/{{ .ID }}/short">
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
@ -1,185 +0,0 @@
|
||||
{{define "courses"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{{ template "html_head" . }}
|
||||
|
||||
<body>
|
||||
{{ template "header" . }}
|
||||
|
||||
<nav class="level">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Courses</p>
|
||||
<p class="title">10k</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Clients</p>
|
||||
<p class="title">1m</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Categories</p>
|
||||
<p class="title">1,024</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Likes</p>
|
||||
<p class="title">Over 9m</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="section">
|
||||
<div class="container block">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs" itemprop="breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
{{ if .LearningTypeName }}
|
||||
<a href="/courses" itemprop="url">
|
||||
<span itemprop="title">
|
||||
Курсы
|
||||
</span>
|
||||
</a>
|
||||
{{ else }}
|
||||
<a>
|
||||
<span itemprop="title" itemprop="url">
|
||||
Курсы
|
||||
</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</li>
|
||||
|
||||
{{ if .LearningTypeName }}
|
||||
<li>
|
||||
{{ if .CourseThematicName }}
|
||||
<a href="/courses/{{.ActiveLearningType}}" itemprop="url">
|
||||
<span itemprop="title">
|
||||
{{ .LearningTypeName }}
|
||||
</span>
|
||||
</a>
|
||||
{{ else }}
|
||||
<a>
|
||||
<span itemprop="title" itemprop="url">
|
||||
{{ .LearningTypeName }}
|
||||
</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
|
||||
{{ if .CourseThematicName }}
|
||||
<li>
|
||||
<a>
|
||||
<span itemprop="title" itemprop="url">
|
||||
{{ .CourseThematicName }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
|
||||
<form id="filter-form" class="columns">
|
||||
<div class="select">
|
||||
<select id="learning-type-filter" name="learning_type">
|
||||
<option value="">Все направления</option>
|
||||
{{ range $t := .AvailableLearningTypes }}
|
||||
<option value="{{$t.ID}}" {{ if eq $t.ID $.ActiveLearningType }}selected{{ end }}>{{ $t.Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{ if .LearningTypeName }}
|
||||
<div class="select">
|
||||
<select id="course-thematic-filter" name="course_thematic">
|
||||
<option value="">Все темы</option>
|
||||
{{ range $t := .AvailableCourseThematics }}
|
||||
<option value="{{$t.ID}}" {{ if eq $t.ID $.ActiveCourseThematic }}selected{{ end }}>{{ $t.Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<button id="go-to-filter" class="button">Перейти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="category-course-list" class="container">
|
||||
{{ range $category := .Categories }}
|
||||
<div class="box">
|
||||
|
||||
<div class="title is-3">
|
||||
<a href="/courses/{{ $category.ID }}">
|
||||
{{ $category.Name }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="subtitle is-6">
|
||||
Some description about the learning category {{ $category.Description }}
|
||||
</div>
|
||||
|
||||
{{ range $subcategory := $category.Subcategories }}
|
||||
<div class="box">
|
||||
<div class="title is-4">
|
||||
<a href="/courses/{{ $category.ID }}/{{ $subcategory.ID }}">
|
||||
{{ $subcategory.Name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="subtitle is-6">Some description about course thematics {{ $subcategory.Description }}</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
{{ range $course := $subcategory.Courses }}
|
||||
{{ template "course_info" $course }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div id="course-info"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
|
||||
<a class="pagination-previous">Previous</a>
|
||||
<a class="pagination-next" href="/courses/?next={{ .NextPageToken }}&per_page=50">Next page</a>
|
||||
</nav>
|
||||
|
||||
{{ template "footer" . }}
|
||||
|
||||
<script>
|
||||
const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
const lt = document.getElementById('learning-type-filter')
|
||||
const ct = document.getElementById('course-thematic-filter');
|
||||
let out = '/courses';
|
||||
if (lt != null && lt.value != '') {
|
||||
out += '/' + lt.value;
|
||||
}
|
||||
if (ct != null && ct.value != '') {
|
||||
out += '/' + ct.value;
|
||||
}
|
||||
document.location.assign(out);
|
||||
return false;
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ff = document.getElementById('filter-form');
|
||||
ff.addEventListener('submit', formFilterOnSubmit);
|
||||
})
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user