diff --git a/.task/checksum/generate b/.task/checksum/generate new file mode 100644 index 0000000..f654668 --- /dev/null +++ b/.task/checksum/generate @@ -0,0 +1 @@ +cf55887b91f81f789d59205c41f8368 diff --git a/Taskfile.yml b/Taskfile.yml index 6e2f54e..33040ce 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -18,12 +18,24 @@ tasks: install_tools: cmds: - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 + - go install github.com/a-h/templ/cmd/templ@v0.2.513 + generate: + cmds: + - "$GOBIN/templ generate" + sources: + - "internal/kurious/ports/http/templ/*.templ" + generates: + - "internal/kurious/ports/http/templ/*.go" check: cmds: - "$GOBIN/golangci-lint run ./..." + deps: + - generate test: cmds: - go test ./internal/... + deps: + - generate build_web: cmds: - go build -o $GOBIN/kuriousweb -v -ldflags '{{.LDFLAGS}}' cmd/kuriweb/*.go diff --git a/cmd/background/main.go b/cmd/background/main.go index 009da69..e3ea585 100644 --- a/cmd/background/main.go +++ b/cmd/background/main.go @@ -93,13 +93,16 @@ func app(ctx context.Context) error { defer xcontext.LogInfo(ctx, log, "finished bprocess") bgProcess.Run() + return nil }) eg.Go(func() error { xcontext.LogInfo(ctx, log, "running cancelation waiter") defer xcontext.LogInfo(ctx, log, "finished cancelation waiter") + <-egctx.Done() + sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*15) defer sdcancel() diff --git a/cmd/kuriweb/http.go b/cmd/kuriweb/http.go index f3a017f..edb22c7 100644 --- a/cmd/kuriweb/http.go +++ b/cmd/kuriweb/http.go @@ -17,6 +17,8 @@ import ( func makePathTemplate(params ...string) string { var sb strings.Builder for _, param := range params { + sb.Grow(len(param) + 3) + sb.WriteRune('/') sb.WriteRune('{') sb.WriteString(param) @@ -26,14 +28,21 @@ func makePathTemplate(params ...string) string { return sb.String() } -func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server { - router := mux.NewRouter() +func setupHTTPWithTempl(srv xhttp.Server, router *mux.Router, log *slog.Logger) { + coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true) + coursesAPI := srv.CoursesByTempl() + + coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet) + coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam) + coursesRouter.HandleFunc(coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet) + coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam) + coursesRouter.HandleFunc(coursesListFullPath, coursesAPI.List).Methods(http.MethodGet) +} + +func setupHTTPWithGoTemplates(srv xhttp.Server, router *mux.Router, log *slog.Logger) { coursesAPI := srv.Courses() - router.Use(mux.CORSMethodMiddleware(router)) - router.Use(middlewareLogger(log)) - // router.HandleFunc("/updatedesc", coursesAPI.UdpateDescription).Methods(http.MethodPost) coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true) coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet) coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam) @@ -46,6 +55,23 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server courseRouter.HandleFunc("/short", coursesAPI.GetShort).Methods(http.MethodGet) courseRouter.HandleFunc("/editdesc", coursesAPI.RenderEditDescription).Methods(http.MethodGet) courseRouter.HandleFunc("/description", coursesAPI.UpdateCourseDescription).Methods(http.MethodPut) +} + +func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server { + router := mux.NewRouter() + + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + router.Use(mux.CORSMethodMiddleware(router)) + router.Use(middlewareLogger(log, cfg.Engine)) + + if cfg.Engine == "templ" { + setupHTTPWithTempl(srv, router, log) + } else { + setupHTTPWithGoTemplates(srv, router, log) + } if cfg.MountLive { fs := http.FileServer(http.Dir("./assets/kurious/static/")) @@ -85,7 +111,7 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server } } -func middlewareLogger(log *slog.Logger) mux.MiddlewareFunc { +func middlewareLogger(log *slog.Logger, engine string) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -93,7 +119,11 @@ func middlewareLogger(log *slog.Logger) mux.MiddlewareFunc { if requestID == "" { requestID = generator.RandomInt64ID() } - ctx = xcontext.WithLogFields(ctx, slog.String("request_id", requestID)) + ctx = xcontext.WithLogFields( + ctx, + slog.String("request_id", requestID), + slog.String("engine", engine), + ) xcontext.LogInfo( ctx, log, "incoming request", diff --git a/go.mod b/go.mod index 9617ea6..8d6aec8 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( ) require ( + github.com/a-h/templ v0.2.513 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.4.0 // indirect diff --git a/go.sum b/go.sum index 2eae5a8..0b17c3c 100644 --- a/go.sum +++ b/go.sum @@ -515,6 +515,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/a-h/templ v0.2.513 h1:ZmwGAOx4NYllnHy+FTpusc4+c5msoMpPIYX0Oy3dNqw= +github.com/a-h/templ v0.2.513/go.mod h1:9gZxTLtRzM3gQxO8jr09Na0v8/jfliS97S9W5SScanM= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= diff --git a/internal/common/config/http.go b/internal/common/config/http.go index ad36b8e..fff7915 100644 --- a/internal/common/config/http.go +++ b/internal/common/config/http.go @@ -3,4 +3,5 @@ package config type HTTP struct { ListenAddr string `json:"listen_addr"` MountLive bool `json:"mount_live"` + Engine string `json:"engine"` } diff --git a/internal/kurious/ports/http/coursev2.go b/internal/kurious/ports/http/coursev2.go new file mode 100644 index 0000000..32db513 --- /dev/null +++ b/internal/kurious/ports/http/coursev2.go @@ -0,0 +1,139 @@ +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" + 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(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, + } + } + }) + + 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 +} + +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.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.BreadcrumbsParams.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, "unab;e 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 + }) + } + + err = xtempl.ListCourses(stats, params).Render(ctx, w) + if handleError(ctx, err, w, c.log, "unable to render list courses") { + return + } +} diff --git a/internal/kurious/ports/http/server.go b/internal/kurious/ports/http/server.go index 01e6123..9b16138 100644 --- a/internal/kurious/ports/http/server.go +++ b/internal/kurious/ports/http/server.go @@ -27,6 +27,10 @@ func (s Server) Courses() courseServer { return courseServer(s) } +func (s Server) CoursesByTempl() courseTemplServer { + return courseTemplServer(s) +} + func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slog.Logger, msg string) bool { if err == nil { return false diff --git a/internal/kurious/ports/http/templ/common.templ b/internal/kurious/ports/http/templ/common.templ new file mode 100644 index 0000000..3936045 --- /dev/null +++ b/internal/kurious/ports/http/templ/common.templ @@ -0,0 +1,22 @@ +package templ + +templ button(title string, attributes templ.Attributes) { + +} + +templ buttonRedirect(id, title string, linkTo string) { + + + @onclickRedirect("origin-link-" + id, linkTo) +} + +script onclickRedirect(id, to string) { + document.getElementById(id).onclick = () => { + location.href = to + } +} diff --git a/internal/kurious/ports/http/templ/common_templ.go b/internal/kurious/ports/http/templ/common_templ.go new file mode 100644 index 0000000..b76bd0f --- /dev/null +++ b/internal/kurious/ports/http/templ/common_templ.go @@ -0,0 +1,116 @@ +// 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("") + 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("") + 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), + } +} diff --git a/internal/kurious/ports/http/templ/header.templ b/internal/kurious/ports/http/templ/header.templ new file mode 100644 index 0000000..8b75f19 --- /dev/null +++ b/internal/kurious/ports/http/templ/header.templ @@ -0,0 +1,62 @@ +package templ + +templ head() { + + + + Courses Aggregator + + + + + + + + +} + +templ navigation() { + +} + +templ footer() { + +} diff --git a/internal/kurious/ports/http/templ/header_templ.go b/internal/kurious/ports/http/templ/header_templ.go new file mode 100644 index 0000000..fe6eac2 --- /dev/null +++ b/internal/kurious/ports/http/templ/header_templ.go @@ -0,0 +1,182 @@ +// 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("") + 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("") + 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("") + 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("") + 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 + }) +} diff --git a/internal/kurious/ports/http/templ/list.templ b/internal/kurious/ports/http/templ/list.templ new file mode 100644 index 0000000..a698f2a --- /dev/null +++ b/internal/kurious/ports/http/templ/list.templ @@ -0,0 +1,209 @@ +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 { +
  • + +
  • + } +} + +templ listCourseHeader(params FilterFormParams) { +
    + @breadcrumb(params.BreadcrumbsParams) + @filterForm(params) +
    +} + +templ breadcrumb(params BreadcrumbsParams) { + +} + +templ filterForm(params FilterFormParams) { +
    +
    + +
    + if !params.ActiveLearningType.Empty() { +
    + +
    + } + @button("goto", templ.Attributes{"id": "go-to-filter"}) +
    +} + +templ listCoursesContainer(categories []CategoryContainer) { +
    + for _, category := range categories { +
    + +
    + This category contains a lot of interesing courses. Check them out! +
    + for _, subcategory := range category.Subcategories { +
    +
    + + { subcategory.Name } + + +
    + for _, course := range subcategory.Courses { + @courseInfoElement(course) + } +
    +
    +
    + } +
    + } +
    +} + +templ ListCourses(s stats, params ListCoursesParams) { + @root(s) { + @listCourseHeader(params.FilterForm) + @listCoursesContainer(params.Categories) + +
    + } +} + +templ root(s stats) { + + + @head() + + @navigation() + +
    + { children... } +
    + @footer() + @breadcrumbsLoad() + + +} + +templ courseInfoElement(params CourseInfo) { +
    +
    +
    +
    + +
    +
    +
    +
    +

    { params.Name }

    +

    oh well

    +
    +
    + if params.FullPrice > 0 { +

    { strconv.Itoa(params.FullPrice) } руб.

    + } else { +

    Бесплатно

    + } +
    + + @buttonRedirect(params.ID, "Show course", params.OriginLink) +
    +
    +
    +} diff --git a/internal/kurious/ports/http/templ/list_templ.go b/internal/kurious/ports/http/templ/list_templ.go new file mode 100644 index 0000000..39a90df --- /dev/null +++ b/internal/kurious/ports/http/templ/list_templ.go @@ -0,0 +1,714 @@ +// 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("
  • ") + 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("
  • ") + 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("
    ") + 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("
    ") + 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("") + 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("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !params.ActiveLearningType.Empty() { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + 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("
    ") + 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("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, category := range categories { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + 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("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, subcategory := range category.Subcategories { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + 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 = templ_7745c5c3_Buffer.WriteString("
    ") + 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("
    ") + 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("") + 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("") + 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("
    ") + 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("
    ") + 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("") + 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("

    ") + 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("

    ") + 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("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if params.FullPrice > 0 { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + 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("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + 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("

    ") + 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 = 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("
    ") + 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 + }) +} diff --git a/internal/kurious/ports/http/templ/vars.go b/internal/kurious/ports/http/templ/vars.go new file mode 100644 index 0000000..ff7b698 --- /dev/null +++ b/internal/kurious/ports/http/templ/vars.go @@ -0,0 +1,95 @@ +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 +} + +type CategoryContainer struct { + CategoryBaseInfo + + Subcategories []SubcategoryContainer +} + +type SubcategoryContainer struct { + CategoryBaseInfo + + Courses []CourseInfo +} + +type ListCoursesParams struct { + FilterForm FilterFormParams + + Categories []CategoryContainer +} diff --git a/internal/kurious/ports/http/templ/vars_test.go b/internal/kurious/ports/http/templ/vars_test.go new file mode 100644 index 0000000..6de60bb --- /dev/null +++ b/internal/kurious/ports/http/templ/vars_test.go @@ -0,0 +1,57 @@ +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) + } + }) + } +} diff --git a/internal/kurious/ports/http/templates/list.tmpl b/internal/kurious/ports/http/templates/list.tmpl index 2f637d0..61a8bfc 100644 --- a/internal/kurious/ports/http/templates/list.tmpl +++ b/internal/kurious/ports/http/templates/list.tmpl @@ -47,7 +47,7 @@ {{ else }} - + Курсы @@ -64,7 +64,7 @@ {{ else }} - + {{ .LearningTypeName }}