From 3a9e01a683b5651dfe339d3cff4687b3fd779e31 Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Fri, 19 Apr 2024 00:13:03 +0300 Subject: [PATCH] rework list courses page to be flatten --- .task/checksum/generate | 2 +- cmd/background/main.go | 6 + cmd/kuriweb/http.go | 30 +- cmd/kuriweb/trace.go | 109 +++-- go.mod | 26 +- go.sum | 52 +- htmlexamples/courses.html | 361 +++++++++----- internal/common/config/trace.go | 38 +- internal/common/decorator/logging.go | 15 +- internal/kurious/adapters/adapters.go | 17 +- internal/kurious/adapters/memory_mapper.go | 25 +- .../kurious/adapters/sqlite_connection.go | 54 +++ .../adapters/sqlite_course_repository.go | 160 ++++--- .../adapters/sqlite_course_repository_test.go | 9 - .../sqlite_learning_category_repository.go | 71 ++- .../sqlite_organization_repository.go | 149 +++++- .../sqlite_organization_repository_test.go | 2 +- .../kurious/adapters/ydb_course_repository.go | 1 - internal/kurious/app/app.go | 5 +- internal/kurious/app/query/listcourses.go | 5 + .../kurious/app/query/listorganizations.go | 8 +- .../app/query/listorganizationstats.go | 48 ++ internal/kurious/domain/organization.go | 8 + internal/kurious/domain/repository.go | 23 +- .../kurious/ports/http/bootstrap/core.templ | 109 ++++- .../ports/http/bootstrap/core_templ.go | 224 ++++++++- .../kurious/ports/http/bootstrap/list.templ | 112 +++-- .../ports/http/bootstrap/list_templ.go | 443 +++++++++++++----- .../kurious/ports/http/bootstrap/main.templ | 4 +- .../ports/http/bootstrap/main_templ.go | 4 +- internal/kurious/ports/http/bootstrap/vars.go | 26 + internal/kurious/ports/http/course.go | 113 ++++- internal/kurious/ports/http/server.go | 13 +- internal/kurious/service/service.go | 5 +- 34 files changed, 1803 insertions(+), 474 deletions(-) create mode 100644 internal/kurious/adapters/sqlite_connection.go create mode 100644 internal/kurious/app/query/listorganizationstats.go diff --git a/.task/checksum/generate b/.task/checksum/generate index e9c2351..543160d 100644 --- a/.task/checksum/generate +++ b/.task/checksum/generate @@ -1 +1 @@ -8d0ebfae489db7ae534fee52ed6ceb3f +c2a78909343b26a169423b19ada40fad diff --git a/cmd/background/main.go b/cmd/background/main.go index f9cc98e..ba65c8f 100644 --- a/cmd/background/main.go +++ b/cmd/background/main.go @@ -29,7 +29,13 @@ func main() { } } +const savingOrganizationIDInternalInsteadOfExternal = false + func app(ctx context.Context) error { + if !savingOrganizationIDInternalInsteadOfExternal { + panic("fix saving ogranization id as external id instead of internal") + } + var cfgpath string if len(os.Args) > 1 { cfgpath = os.Args[1] diff --git a/cmd/kuriweb/http.go b/cmd/kuriweb/http.go index faf2f90..9182c38 100644 --- a/cmd/kuriweb/http.go +++ b/cmd/kuriweb/http.go @@ -107,11 +107,21 @@ func middlewareCustomWriterInjector() mux.MiddlewareFunc { } } +type attributeStringKey string + +func (k attributeStringKey) Value(value string) attribute.KeyValue { + return attribute.String(string(k), value) +} + func middlewareTrace() mux.MiddlewareFunc { - reqidAttr := attribute.Key("http.request_id") - statusAttr := attribute.Key("http.status_code") - payloadAttr := attribute.Key("http.payload_size") - pathAttr := attribute.Key("http.template_path") + methodAttr := attributeStringKey("http.request.method") + reqidAttr := attributeStringKey("http.request_id") + routeAttr := attributeStringKey("http.route") + queryAttr := attributeStringKey("url.query") + pathAttr := attributeStringKey("http.path") + uaAttr := attributeStringKey("user_agent.original") + statusAttr := attribute.Key("http.response.status_code") + payloadAttr := attribute.Key("http.response_content_length") return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -120,14 +130,18 @@ func middlewareTrace() mux.MiddlewareFunc { var span trace.Span route := mux.CurrentRoute(r) - hname := route.GetName() hpath, _ := route.GetPathTemplate() ctx, span = webtracer.Start( - ctx, "http."+hname, + ctx, r.Method+" "+hpath, trace.WithAttributes( - reqidAttr.String(reqid), - pathAttr.String(hpath), + methodAttr.Value(r.Method), + reqidAttr.Value(reqid), + routeAttr.Value(hpath), + pathAttr.Value(r.URL.Path), + queryAttr.Value(r.URL.RawQuery), + uaAttr.Value(r.UserAgent()), ), + trace.WithSpanKind(trace.SpanKindServer), ) defer span.End() diff --git a/cmd/kuriweb/trace.go b/cmd/kuriweb/trace.go index b9202de..7e1fde3 100644 --- a/cmd/kuriweb/trace.go +++ b/cmd/kuriweb/trace.go @@ -8,18 +8,19 @@ import ( "time" "git.loyso.art/frx/kurious/internal/common/config" - "git.loyso.art/frx/kurious/pkg/xdefault" "github.com/gorilla/mux" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" ) var webtracer = otel.Tracer("kuriweb.http") @@ -46,19 +47,26 @@ func setupOtelSDK(ctx context.Context, cfg config.Trace) (shutdown shutdownFunc, prop := newPropagator() otel.SetTextMapPropagator(prop) - tracerProvider, err := newTraceProvider(ctx, cfg.Endpoint, cfg.LicenseKey) + tracerProvider, err := newCommonTraceProvider(ctx, TraceProviderParams{ + Endpoint: cfg.Endpoint, + Type: cfg.Type, + AuthHeader: cfg.APIHeader, + APIKey: cfg.APIKey, + }) if err != nil { return nil, handleError(err) } shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) otel.SetTracerProvider(tracerProvider) - meterProvider, err := newMeterProvider() - if err != nil { - return nil, handleError(err) + if cfg.ShowMetrics { + meterProvider, err := newMeterProvider() + if err != nil { + return nil, handleError(err) + } + shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) + otel.SetMeterProvider(meterProvider) } - shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) - otel.SetMeterProvider(meterProvider) return shutdown, nil } @@ -70,46 +78,75 @@ func newPropagator() propagation.TextMapPropagator { ) } -const defaultNewRelicEndpoint = "otlp.eu01.nr-data.net:443" +type TraceProviderParams struct { + Type config.TraceClientType + Endpoint string + APIKey string + AuthHeader string +} -func newTraceProvider(ctx context.Context, endpoint, licensekey string) (traceProvider *trace.TracerProvider, err error) { - opts := make([]trace.TracerProviderOption, 0, 2) +func newCommonTraceProvider(ctx context.Context, params TraceProviderParams) (tp *trace.TracerProvider, err error) { + r, err := resource.New( + ctx, + resource.WithDetectors( + resource.StringDetector(semconv.SchemaURL, semconv.ServiceNameKey, func() (string, error) { + return "bigstats:kuriweb", nil + }), + ), + ) + if err != nil { + return nil, fmt.Errorf("making new resource: %w", err) + } + + r, err = resource.Merge(resource.Default(), r) + if err != nil { + return nil, fmt.Errorf("merging resources: %w", err) + } + + opts := make([]trace.TracerProviderOption, 0, 4) opts = append( opts, trace.WithSampler(trace.AlwaysSample()), - trace.WithResource(resource.Default()), + trace.WithResource(r), ) - if licensekey != "" { - endpoint = xdefault.WithFallback(endpoint, defaultNewRelicEndpoint) - client, err := otlptracegrpc.New( - ctx, - otlptracegrpc.WithEndpoint(endpoint), - otlptracegrpc.WithHeaders(map[string]string{ - "api-key": licensekey, - }), - ) - if err != nil { - return nil, fmt.Errorf("making grpc client: %w", err) + if params.Type != config.TraceClientTypeUnset { + var spanExporter trace.SpanExporter + var headers map[string]string + + if params.AuthHeader != "" { + headers = make(map[string]string, 1) + headers[params.AuthHeader] = params.APIKey } - opts = append(opts, trace.WithBatcher(client, trace.WithBatchTimeout(time.Second*10))) - } else { - traceExporter, err := stdouttrace.New( - stdouttrace.WithPrettyPrint()) + switch params.Type { + case config.TraceClientTypeGRPC: + spanExporter, err = otlptracegrpc.New( + ctx, + otlptracegrpc.WithEndpointURL(params.Endpoint), + otlptracegrpc.WithHeaders(headers), + ) + case config.TraceClientTypeHTTP: + httpClient := otlptracehttp.NewClient( + otlptracehttp.WithEndpointURL(params.Endpoint), + otlptracehttp.WithHeaders(headers), + ) + spanExporter, err = otlptrace.New(ctx, httpClient) + case config.TraceClientTypeStdout: + spanExporter, err = stdouttrace.New(stdouttrace.WithPrettyPrint()) + default: + return nil, fmt.Errorf("unsupported provider type") + } if err != nil { - return nil, err + return nil, fmt.Errorf("making trace exporter: %w", err) } - opts = append( - opts, - trace.WithBatcher(traceExporter, trace.WithBatchTimeout(time.Second*5)), - ) + opts = append(opts, trace.WithBatcher(spanExporter)) } - traceProvider = trace.NewTracerProvider(opts...) + tp = trace.NewTracerProvider(opts...) - return traceProvider, nil + return tp, nil } func newMeterProvider() (*metric.MeterProvider, error) { @@ -127,6 +164,6 @@ func newMeterProvider() (*metric.MeterProvider, error) { } func muxHandleFunc(router *mux.Router, name, path string, hf http.HandlerFunc) *mux.Route { - h := otelhttp.WithRouteTag(path, hf) - return router.Handle(path, h).Name(name) + // h := otelhttp.WithRouteTag(path, hf) + return router.Handle(path, hf).Name(name) } diff --git a/go.mod b/go.mod index f650aa3..6ea7f8a 100644 --- a/go.mod +++ b/go.mod @@ -12,15 +12,16 @@ require ( github.com/teris-io/cli v1.0.1 github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 github.com/ydb-platform/ydb-go-yc v0.12.1 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 - go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel v1.25.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 - go.opentelemetry.io/otel/sdk v1.24.0 + go.opentelemetry.io/otel/sdk v1.25.0 go.opentelemetry.io/otel/sdk/metric v1.24.0 - go.opentelemetry.io/otel/trace v1.24.0 - golang.org/x/net v0.22.0 + go.opentelemetry.io/otel/trace v1.25.0 + golang.org/x/net v0.24.0 golang.org/x/sync v0.6.0 golang.org/x/time v0.5.0 modernc.org/sqlite v1.29.3 @@ -30,11 +31,9 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -47,14 +46,13 @@ require ( github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 // indirect github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd // indirect github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v1.1.0 // indirect - golang.org/x/sys v0.18.0 // indirect + go.opentelemetry.io/otel/metric v1.25.0 // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect - google.golang.org/grpc v1.62.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect diff --git a/go.sum b/go.sum index 173981b..f30c88e 100644 --- a/go.sum +++ b/go.sum @@ -575,8 +575,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -641,8 +639,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -846,31 +842,31 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= +go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0 h1:JYE2HM7pZbOt5Jhk8ndWZTUWYOVift2cHjXVMkPdmdc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0/go.mod h1:yMb/8c6hVsnma0RpsBMNo0fEiQKeclawtgaIaOp2MLY= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= +go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= +go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= +go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8= go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= +go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -1000,8 +996,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1126,8 +1122,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1430,10 +1426,10 @@ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38= -google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= +google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1474,8 +1470,8 @@ google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsA google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/htmlexamples/courses.html b/htmlexamples/courses.html index ae1dfdb..258b6f4 100644 --- a/htmlexamples/courses.html +++ b/htmlexamples/courses.html @@ -1,142 +1,277 @@ + + Test page + + + + + + - - Test page - - - - - - - - -
- -
- -
-
- -
- -
-
-
- Filter categories - - - - - +
-
-
+ + -
-

Languages

-

A languages category provides all courses to help learn language

+
+
+ +
+ +
+
+
+ Filter categories + + + + + +
+
+
-
- - +
+ +
+
+ -
- + + + + + +
- -
+ +
+ +
Promocodes
-
-

Japanese

-

Looking for a course to learn japanese language?

- -
-
-
- ... -
-
Card title with a long naming
-
- Open > - 500$ -
+
+
+
+ ... +
+
Card title with a long naming
+
+ Open > + 500$
+
-
-
+ +

+
+
+
+
+
+
+ Courses +
+

+ Welcome to Courses, your gateway to learning! Explore a diverse + range of courses and advance your skills with us. Join our + community and transform your life through education. +

+
-
-
-

(c) All right reserved

+
+
Useful links
+

+ Pricing +

+

+ Settings +

+

+ Orders +

+

+ Help +

+
+ +
+
Contact
+

New York, NY 10012, US

+

+ + info@example.com +

+

+ 01 234 567 88

+

+ 01 234 567 89

+
+
+
+
+ +
+ © 2024 Copyright: + kursov.net
-
- - + diff --git a/internal/common/config/trace.go b/internal/common/config/trace.go index 5749c84..844e740 100644 --- a/internal/common/config/trace.go +++ b/internal/common/config/trace.go @@ -1,6 +1,38 @@ package config -type Trace struct { - Endpoint string `json:"endpoint"` - LicenseKey string `json:"license_key"` +import "errors" + +type TraceClientType uint8 + +const ( + TraceClientTypeUnset TraceClientType = iota + TraceClientTypeHTTP + TraceClientTypeGRPC + TraceClientTypeStdout +) + +func (t *TraceClientType) UnmarshalText(data []byte) error { + dataStr := string(data) + switch dataStr { + case "http": + *t = TraceClientTypeHTTP + case "grpc": + *t = TraceClientTypeGRPC + case "stdout": + *t = TraceClientTypeStdout + case "": + default: + return errors.New("unsupported value " + dataStr) + } + + return nil +} + +type Trace struct { + Endpoint string `json:"endpoint"` + APIKey string `json:"api_key"` + APIHeader string `json:"api_header"` + Type TraceClientType `json:"type"` + + ShowMetrics bool `json:"show_metrics"` } diff --git a/internal/common/decorator/logging.go b/internal/common/decorator/logging.go index 3fc3285..fa54615 100644 --- a/internal/common/decorator/logging.go +++ b/internal/common/decorator/logging.go @@ -16,11 +16,10 @@ import ( ) var ( - commandAttribute = attribute.Key("command_name") - queryAttribute = attribute.Key("query_name") - argsAttribute = attribute.Key("args") + nameAttribute = attribute.Key("cq.name") + argsAttribute = attribute.Key("cq.args") - apiTracer = otel.Tracer("cq") + apiTracer = otel.Tracer("api") ) type commandLoggingDecorator[T any] struct { @@ -37,9 +36,9 @@ func (c commandLoggingDecorator[T]) Handle(ctx context.Context, cmd T) (err erro _ = json.NewEncoder(&argsBuilder).Encode(cmd) var span trace.Span - ctx, span = apiTracer.Start(ctx, handlerName) + ctx, span = apiTracer.Start(ctx, "command "+handlerName) span.SetAttributes( - commandAttribute.String(handlerName), + nameAttribute.String(handlerName), argsAttribute.String(argsBuilder.String()), ) @@ -73,9 +72,9 @@ func (q queryLoggingDecorator[Q, U]) Handle(ctx context.Context, query Q) (entit _ = json.NewEncoder(&argsBuilder).Encode(query) var span trace.Span - ctx, span = apiTracer.Start(ctx, handlerName) + ctx, span = apiTracer.Start(ctx, "query "+handlerName) span.SetAttributes( - queryAttribute.String(handlerName), + nameAttribute.String(handlerName), argsAttribute.String(argsBuilder.String()), ) diff --git a/internal/kurious/adapters/adapters.go b/internal/kurious/adapters/adapters.go index bc152a8..5b5908c 100644 --- a/internal/kurious/adapters/adapters.go +++ b/internal/kurious/adapters/adapters.go @@ -1,6 +1,21 @@ package adapters -import "strings" +import ( + "strings" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" +) + +var dbTracer = otel.Tracer("adapters.db") + +var ( + dbSystemAttr = attribute.Key("db.system") + dbNameAttr = attribute.Key("db.name") + dbStatementAttr = attribute.Key("db.statement") + dbOperationAttr = attribute.Key("db.operation") + dbTableAttr = attribute.Key("db.sql.table") +) type domainer[T any] interface { AsDomain() T diff --git a/internal/kurious/adapters/memory_mapper.go b/internal/kurious/adapters/memory_mapper.go index 886709d..b6d012c 100644 --- a/internal/kurious/adapters/memory_mapper.go +++ b/internal/kurious/adapters/memory_mapper.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "git.loyso.art/frx/kurious/internal/common/xslices" "git.loyso.art/frx/kurious/internal/kurious/domain" ) @@ -29,19 +30,26 @@ func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepo m.stats = map[string]domain.LearningTypeStat{} m.courseThematicByLearningType = map[string]string{} - var nextPageToken string + var offset int for { result, err := cr.List(ctx, domain.ListCoursesParams{ LearningType: "", CourseThematic: "", - NextPageToken: nextPageToken, - Limit: batchSize, + Limit: batchSize + 1, + Offset: offset, }) if err != nil { return fmt.Errorf("listing courses: %w", err) } - m.totalCount += len(result.Courses) - for _, course := range result.Courses { + + courses := result.Courses + if len(result.Courses) >= batchSize { + courses = result.Courses[:batchSize] + } + + m.totalCount += len(courses) + + xslices.ForEach(courses, func(course domain.Course) { stat, ok := m.stats[course.LearningTypeID] stat.Count++ if !ok { @@ -50,11 +58,12 @@ func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepo stat.CourseThematic[course.ThematicID]++ m.stats[course.LearningTypeID] = stat m.courseThematicByLearningType[course.ThematicID] = course.LearningTypeID - } - if len(result.Courses) < batchSize { + }) + + if len(result.Courses) != batchSize+1 { break } - nextPageToken = result.NextPageToken + offset += batchSize } return nil diff --git a/internal/kurious/adapters/sqlite_connection.go b/internal/kurious/adapters/sqlite_connection.go new file mode 100644 index 0000000..a3d91f2 --- /dev/null +++ b/internal/kurious/adapters/sqlite_connection.go @@ -0,0 +1,54 @@ +package adapters + +import ( + "context" + "fmt" + "log/slog" + "time" + + "git.loyso.art/frx/kurious/internal/common/config" + "git.loyso.art/frx/kurious/migrations/sqlite" + "git.loyso.art/frx/kurious/pkg/xdefault" + "go.opentelemetry.io/otel/attribute" + + "github.com/jmoiron/sqlx" + _ "modernc.org/sqlite" +) + +type sqliteConnection struct { + db *sqlx.DB + shutdownTimeout time.Duration + log *slog.Logger +} + +func NewSqliteConnection(ctx context.Context, cfg config.Sqlite, log *slog.Logger) (*sqliteConnection, error) { + conn, err := sqlx.Open("sqlite", cfg.DSN) + if err != nil { + return nil, fmt.Errorf("openning db connection: %w", err) + } + + err = sqlite.RunMigrations(ctx, conn.DB, log) + if err != nil { + return nil, fmt.Errorf("running migrations: %w", err) + } + + return &sqliteConnection{ + db: conn, + log: log, + shutdownTimeout: xdefault.WithFallback(cfg.ShutdownTimeout, defaultShutdownTimeout), + }, nil +} + +func (c *sqliteConnection) Close() error { + _, cancel := context.WithTimeout(context.Background(), c.shutdownTimeout) + defer cancel() + + return c.db.Close() +} + +func getSqliteBaseAttributes() []attribute.KeyValue { + return []attribute.KeyValue{ + dbSystemAttr.String("sqlite"), + dbNameAttr.String("courses"), + } +} diff --git a/internal/kurious/adapters/sqlite_course_repository.go b/internal/kurious/adapters/sqlite_course_repository.go index ea6ae5e..e885226 100644 --- a/internal/kurious/adapters/sqlite_course_repository.go +++ b/internal/kurious/adapters/sqlite_course_repository.go @@ -9,52 +9,15 @@ import ( "strings" "time" - "git.loyso.art/frx/kurious/internal/common/config" "git.loyso.art/frx/kurious/internal/common/nullable" "git.loyso.art/frx/kurious/internal/common/xcontext" "git.loyso.art/frx/kurious/internal/kurious/domain" - "git.loyso.art/frx/kurious/migrations/sqlite" - "git.loyso.art/frx/kurious/pkg/xdefault" "github.com/jmoiron/sqlx" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - _ "modernc.org/sqlite" + "go.opentelemetry.io/otel/trace" ) -var sqliteTracer = otel.Tracer("sqlite") - -type sqliteConnection struct { - db *sqlx.DB - shutdownTimeout time.Duration - log *slog.Logger -} - -func NewSqliteConnection(ctx context.Context, cfg config.Sqlite, log *slog.Logger) (*sqliteConnection, error) { - conn, err := sqlx.Open("sqlite", cfg.DSN) - if err != nil { - return nil, fmt.Errorf("openning db connection: %w", err) - } - - err = sqlite.RunMigrations(ctx, conn.DB, log) - if err != nil { - return nil, fmt.Errorf("running migrations: %w", err) - } - - return &sqliteConnection{ - db: conn, - log: log, - shutdownTimeout: xdefault.WithFallback(cfg.ShutdownTimeout, defaultShutdownTimeout), - }, nil -} - -func (c *sqliteConnection) Close() error { - _, cancel := context.WithTimeout(context.Background(), c.shutdownTimeout) - defer cancel() - - return c.db.Close() -} - func (c *sqliteConnection) CourseRepository() *sqliteCourseRepository { return &sqliteCourseRepository{ db: c.db, @@ -73,7 +36,15 @@ func (r *sqliteCourseRepository) List( ) (result domain.ListCoursesResult, err error) { const queryTemplate = `SELECT %s from courses WHERE 1=1` - ctx, span := sqliteTracer.Start(ctx, "sqlite.list") + ctx, span := dbTracer.Start( + ctx, "list courses.courses", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + )..., + ), + ) defer func() { if err != nil { span.RecordError(err) @@ -81,10 +52,6 @@ func (r *sqliteCourseRepository) List( span.End() }() - if params.NextPageToken != "" && params.Offset > 0 { - panic("could not use next_page_token and offset at the same time") - } - query := fmt.Sprintf(queryTemplate, coursesFieldsStr) args := make([]any, 0, 6) if params.LearningType != "" { @@ -99,12 +66,16 @@ func (r *sqliteCourseRepository) List( args = append(args, params.OrganizationID) query += " AND organization_id = ?" } - if params.NextPageToken != "" { - args = append(args, params.NextPageToken) - query += " AND id > ?" + + if params.OrderBy == "" { + params.OrderBy = "id" } - query += " ORDER BY course_thematic, learning_type, id ASC" + direction := "ASC" + if !params.Ascending { + direction = "DESC" + } + query += " ORDER BY " + params.OrderBy + " " + direction if params.Limit > 0 { query += " LIMIT ?" @@ -115,9 +86,8 @@ func (r *sqliteCourseRepository) List( args = append(args, params.Offset) } - span.SetAttributes( - attribute.String("query", query), - ) + span.SetAttributes(dbStatementAttr.String(query)) + scanF := func(s rowsScanner) (err error) { var cdb sqliteCourseDB err = s.StructScan(&cdb) @@ -144,8 +114,8 @@ func (r *sqliteCourseRepository) List( } span.SetAttributes( - attribute.Int("items_count", len(result.Courses)), - attribute.Int("total_items", result.Count), + attribute.Int("db.items_count", len(result.Courses)), + attribute.Int("db.total_items", result.Count), ) return result, nil } @@ -155,6 +125,23 @@ func (r *sqliteCourseRepository) ListLearningTypes( ) (result domain.ListLearningTypeResult, err error) { const query = "SELECT DISTINCT learning_type FROM courses" + ctx, span := dbTracer.Start( + ctx, "list_learning_types courses.courses", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + err = r.db.SelectContext(ctx, &result.LearningTypeIDs, query) if err != nil { return result, fmt.Errorf("executing query: %w", err) @@ -176,6 +163,23 @@ func (r *sqliteCourseRepository) ListCourseThematics( query += " AND learning_type = ?" } + ctx, span := dbTracer.Start( + ctx, "list courses.course_thematic", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + err = r.db.SelectContext(ctx, &result.CourseThematicIDs, query, args...) if err != nil { return result, fmt.Errorf("executing query: %w", err) @@ -191,6 +195,24 @@ func (r *sqliteCourseRepository) Get( const queryTemplate = `SELECT %s FROM courses WHERE id = ?` query := fmt.Sprintf(queryTemplate, coursesFieldsStr) + + ctx, span := dbTracer.Start( + ctx, "get courses.courses", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + var courseDB sqliteCourseDB err = r.db.GetContext(ctx, &courseDB, query, id) if err != nil { @@ -259,14 +281,6 @@ func (r *sqliteCourseRepository) Delete(ctx context.Context, id string) error { func (r *sqliteCourseRepository) listCount(ctx context.Context, params domain.ListCoursesParams) (count int, err error) { const queryTemplate = `SELECT COUNT(id) FROM courses WHERE 1=1` - ctx, span := sqliteTracer.Start(ctx, "sqlite.listCount") - defer func() { - if err != nil { - span.RecordError(err) - } - span.End() - }() - query := queryTemplate args := make([]any, 0, 6) @@ -283,6 +297,23 @@ func (r *sqliteCourseRepository) listCount(ctx context.Context, params domain.Li query += " AND organization_id = ?" } + ctx, span := dbTracer.Start( + ctx, "list_count courses.courses", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + err = r.db.GetContext(ctx, &count, query, args...) if err != nil { return count, fmt.Errorf("sending query: %w", err) @@ -442,3 +473,12 @@ func (c *sqliteCourseDB) FromDomain(d domain.Course) { }, } } + +func (c *sqliteCourseRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue { + outbase := append( + getSqliteBaseAttributes(), + dbTableAttr.String("courses"), + ) + + return append(outbase, custom...) +} diff --git a/internal/kurious/adapters/sqlite_course_repository_test.go b/internal/kurious/adapters/sqlite_course_repository_test.go index 4953abb..8691c60 100644 --- a/internal/kurious/adapters/sqlite_course_repository_test.go +++ b/internal/kurious/adapters/sqlite_course_repository_test.go @@ -104,13 +104,4 @@ func (s *sqliteCourseRepositorySuite) TestListLimitOffset() { result, err := cr.List(s.ctx, params) s.NoError(err) s.Empty(result.Courses) - - params.Offset = 0 - for i := 0; i < listparts; i++ { - result, err := cr.List(s.ctx, params) - s.NoError(err) - s.Len(result.Courses, listparts) - - params.NextPageToken = result.NextPageToken - } } diff --git a/internal/kurious/adapters/sqlite_learning_category_repository.go b/internal/kurious/adapters/sqlite_learning_category_repository.go index 3c98749..b3e9572 100644 --- a/internal/kurious/adapters/sqlite_learning_category_repository.go +++ b/internal/kurious/adapters/sqlite_learning_category_repository.go @@ -12,6 +12,8 @@ import ( "git.loyso.art/frx/kurious/internal/kurious/domain" "github.com/jmoiron/sqlx" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) var ( @@ -50,7 +52,7 @@ type sqliteLearingCategoryRepository struct { log *slog.Logger } -func (r *sqliteLearingCategoryRepository) Upsert(ctx context.Context, c domain.LearningCategory) error { +func (r *sqliteLearingCategoryRepository) Upsert(ctx context.Context, c domain.LearningCategory) (err error) { const queryTemplate = "INSERT INTO learning_categories (%s)" + " VALUES (%s)" + " ON CONFLICT(id) DO UPDATE" + @@ -64,7 +66,24 @@ func (r *sqliteLearingCategoryRepository) Upsert(ctx context.Context, c domain.L strings.TrimSuffix(strings.Repeat("?,", len(learningCategoryColumns)), ","), ) - _, err := r.db.ExecContext( + ctx, span := dbTracer.Start( + ctx, "upsert courses.learning_categories", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("INSERT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + + _, err = r.db.ExecContext( ctx, query, c.ID, nullableValueAsString(c.Logo), @@ -82,6 +101,23 @@ func (r *sqliteLearingCategoryRepository) List(ctx context.Context) (out []domai query := fmt.Sprintf(queryTemplate, learningCategoryColumnsStr) + ctx, span := dbTracer.Start( + ctx, "list courses.learning_categories", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + var categories []learningCategoryDB err = r.db.SelectContext(ctx, &categories, query) if err != nil { @@ -93,13 +129,31 @@ func (r *sqliteLearingCategoryRepository) List(ctx context.Context) (out []domai return out, nil } -func (r *sqliteLearingCategoryRepository) Get(ctx context.Context, id string) (domain.LearningCategory, error) { +func (r *sqliteLearingCategoryRepository) Get(ctx context.Context, id string) (category domain.LearningCategory, err error) { const queryTemplate = "SELECT %s FROM learning_categories WHERE id = ?;" query := fmt.Sprintf(queryTemplate, learningCategoryColumnsStr) + ctx, span := dbTracer.Start( + ctx, "get courses.learning_categories", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + var cdb learningCategoryDB - err := r.db.GetContext(ctx, &cdb, query, id) + err = r.db.GetContext(ctx, &cdb, query, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return domain.LearningCategory{}, domain.ErrNotFound @@ -109,3 +163,12 @@ func (r *sqliteLearingCategoryRepository) Get(ctx context.Context, id string) (d return cdb.AsDomain(), nil } + +func (c *sqliteLearingCategoryRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue { + outbase := append( + getSqliteBaseAttributes(), + dbTableAttr.String("learning_categories"), + ) + + return append(outbase, custom...) +} diff --git a/internal/kurious/adapters/sqlite_organization_repository.go b/internal/kurious/adapters/sqlite_organization_repository.go index 86c3003..172a158 100644 --- a/internal/kurious/adapters/sqlite_organization_repository.go +++ b/internal/kurious/adapters/sqlite_organization_repository.go @@ -6,10 +6,13 @@ import ( "errors" "fmt" "log/slog" + "strings" "time" "git.loyso.art/frx/kurious/internal/common/xslices" "git.loyso.art/frx/kurious/internal/kurious/domain" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "github.com/jmoiron/sqlx" ) @@ -31,6 +34,22 @@ var ( organizationColumnsArgsStr = namedArgColumns(organizationColumns) ) +type organizationStatDB struct { + ID string `db:"id"` + ExternalID sql.NullString `db:"external_id"` + Name string `db:"name"` + CoursesCount uint64 `db:"courses_count"` +} + +func (s organizationStatDB) AsDomain() domain.OrganizationStat { + return domain.OrganizationStat{ + ID: s.ID, + ExternalID: nullStringAsDomain(s.ExternalID), + Name: s.Name, + CoursesCount: s.CoursesCount, + } +} + type organizationDB struct { ID string `db:"id"` ExternalID sql.NullString `db:"external_id"` @@ -83,10 +102,73 @@ type sqliteOrganizationRepository struct { log *slog.Logger } -func (r *sqliteOrganizationRepository) List(ctx context.Context) (out []domain.Organization, err error) { - const queryTemplate = `SELECT %s FROM organizations` +func (r *sqliteOrganizationRepository) ListStats( + ctx context.Context, + params domain.ListOrganizationsParams, +) (out []domain.OrganizationStat, err error) { + const query = `SELECT o.id as id, o.external_id as external_id, o.name as name, COUNT(c.id) as courses_count` + + ` FROM organizations o` + + ` INNER JOIN courses c ON o.id = c.organization_id` + + ` GROUP BY o.id, o.external_id, o.name` + + ` ORDER BY COUNT(c.id) DESC` + + ctx, span := dbTracer.Start( + ctx, "list_stats courses", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + + var stats []organizationStatDB + err = r.db.SelectContext(ctx, &stats, query) + if err != nil { + return nil, fmt.Errorf("executing query: %w", err) + } + + return xslices.Map(stats, asDomainFunc), nil +} + +func (r *sqliteOrganizationRepository) List(ctx context.Context, params domain.ListOrganizationsParams) (out []domain.Organization, err error) { + const queryTemplate = `SELECT %s FROM organizations WHERE 1=1` query := fmt.Sprintf(queryTemplate, organizationColumnsStr) + args := make([]any, 0, len(params.IDs)) + if len(params.IDs) > 0 { + args = append( + args, + xslices.Map(params.IDs, func(t string) any { return t })..., + ) + queryParam := strings.TrimSuffix(strings.Repeat("?,", len(args)), ",") + query += " AND id IN (" + queryParam + ")" + } + + ctx, span := dbTracer.Start( + ctx, "list courses.organizations", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + organizations := make([]organizationDB, 0, 1<<8) err = r.db.SelectContext(ctx, &organizations, query) if err != nil { @@ -109,6 +191,23 @@ func (r *sqliteOrganizationRepository) Get(ctx context.Context, params domain.Ge query += " AND external_id = ?" } + ctx, span := dbTracer.Start( + ctx, "get courses.organizations", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("SELECT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + var orgdb organizationDB err = r.db.GetContext(ctx, &orgdb, query, args...) if err != nil { @@ -125,6 +224,23 @@ func (r *sqliteOrganizationRepository) Create(ctx context.Context, params domain const queryTemplate = `INSERT INTO organizations (%[1]s) VALUES (%[2]s) RETURNING %[1]s` query := fmt.Sprintf(queryTemplate, organizationColumnsStr, organizationColumnsArgsStr) + ctx, span := dbTracer.Start( + ctx, "create courses.organizations", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("INSERT"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + stmt, err := r.db.PrepareNamedContext(ctx, query) if err != nil { return out, fmt.Errorf("preparing statement: %w", err) @@ -151,8 +267,26 @@ func (r *sqliteOrganizationRepository) Create(ctx context.Context, params domain return orgdb.AsDomain(), nil } -func (r *sqliteOrganizationRepository) Delete(ctx context.Context, id string) error { +func (r *sqliteOrganizationRepository) Delete(ctx context.Context, id string) (err error) { const query = `DELETE FROM organizations WHERE id = ?` + + ctx, span := dbTracer.Start( + ctx, "delete courses.organizations", + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + r.mergeAttributes( + dbOperationAttr.String("DELETE"), + dbStatementAttr.String(query), + )..., + ), + ) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + result, err := r.db.ExecContext(ctx, query, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -168,3 +302,12 @@ func (r *sqliteOrganizationRepository) Delete(ctx context.Context, id string) er return nil } + +func (c *sqliteOrganizationRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue { + outbase := append( + getSqliteBaseAttributes(), + dbTableAttr.String("organizaitons"), + ) + + return append(outbase, custom...) +} diff --git a/internal/kurious/adapters/sqlite_organization_repository_test.go b/internal/kurious/adapters/sqlite_organization_repository_test.go index 98fa8c7..4914576 100644 --- a/internal/kurious/adapters/sqlite_organization_repository_test.go +++ b/internal/kurious/adapters/sqlite_organization_repository_test.go @@ -51,7 +51,7 @@ func (s *sqliteOrganzationRepositorySuite) TestList() { orgsdb = append(orgsdb, gotOrg) } - gotOrgs, err := s.connection.Organization().List(s.ctx) + gotOrgs, err := s.connection.Organization().List(s.ctx, domain.ListOrganizationsParams{}) s.NoError(err) compareF := func(lhs, rhs domain.Organization) int { diff --git a/internal/kurious/adapters/ydb_course_repository.go b/internal/kurious/adapters/ydb_course_repository.go index 097fc83..46c6e4b 100644 --- a/internal/kurious/adapters/ydb_course_repository.go +++ b/internal/kurious/adapters/ydb_course_repository.go @@ -178,7 +178,6 @@ func (r *ydbCourseRepository) List( opts = append( opts, - table.ValueParam("$id", types.TextValue(params.NextPageToken)), table.ValueParam("$limit", types.Int32Value(int32(params.Limit))), ) diff --git a/internal/kurious/app/app.go b/internal/kurious/app/app.go index cf3e978..7d74282 100644 --- a/internal/kurious/app/app.go +++ b/internal/kurious/app/app.go @@ -21,8 +21,9 @@ type Queries struct { ListCourseThematics query.ListCourseThematicsHandler ListCourseStatistics query.ListCoursesStatsHandler - ListOrganzations query.ListOrganizationsHandler - GetOrganization query.GetOrganizationHandler + ListOrganzations query.ListOrganizationsHandler + ListOrganizationsStats query.ListOrganizationsStatsHandler + GetOrganization query.GetOrganizationHandler } type Application struct { diff --git a/internal/kurious/app/query/listcourses.go b/internal/kurious/app/query/listcourses.go index 701989e..a701fa9 100644 --- a/internal/kurious/app/query/listcourses.go +++ b/internal/kurious/app/query/listcourses.go @@ -17,6 +17,9 @@ type ListCourse struct { OrganizationID string Keyword string + OrderBy string + Ascending bool + Limit int Offset int NextPageToken string @@ -57,6 +60,8 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do OrganizationID: query.OrganizationID, Limit: query.Limit, Offset: query.Offset, + OrderBy: query.OrderBy, + Ascending: query.Ascending, }) if err != nil { return out, fmt.Errorf("listing courses: %w", err) diff --git a/internal/kurious/app/query/listorganizations.go b/internal/kurious/app/query/listorganizations.go index 7f095e1..d0d13d0 100644 --- a/internal/kurious/app/query/listorganizations.go +++ b/internal/kurious/app/query/listorganizations.go @@ -9,7 +9,9 @@ import ( "git.loyso.art/frx/kurious/internal/kurious/domain" ) -type ListOrganizations struct{} +type ListOrganizations struct { + IDs []string +} type ListOrganizationsHandler decorator.QueryHandler[ListOrganizations, []domain.Organization] @@ -29,7 +31,9 @@ func NewListOrganizationsHandler( } func (h listOrganizationsHandler) Handle(ctx context.Context, query ListOrganizations) ([]domain.Organization, error) { - organizations, err := h.repo.List(ctx) + organizations, err := h.repo.List(ctx, domain.ListOrganizationsParams{ + IDs: query.IDs, + }) if err != nil { return nil, fmt.Errorf("listing organizations: %w", err) } diff --git a/internal/kurious/app/query/listorganizationstats.go b/internal/kurious/app/query/listorganizationstats.go new file mode 100644 index 0000000..dd93694 --- /dev/null +++ b/internal/kurious/app/query/listorganizationstats.go @@ -0,0 +1,48 @@ +package query + +import ( + "context" + "fmt" + "log/slog" + + "git.loyso.art/frx/kurious/internal/common/decorator" + "git.loyso.art/frx/kurious/internal/kurious/domain" +) + +type ListOrganizationsStats struct { + IDs []string +} + +type ListOrganizationsStatsHandler decorator.QueryHandler[ + ListOrganizationsStats, + []domain.OrganizationStat, +] + +type listOrganizationsStatsHandler struct { + repo domain.OrganizationRepository +} + +func NewListOrganizationsStatsHandler( + repo domain.OrganizationRepository, + log *slog.Logger, +) ListOrganizationsStatsHandler { + h := listOrganizationsStatsHandler{ + repo: repo, + } + + return decorator.AddQueryDecorators(h, log) +} + +func (h listOrganizationsStatsHandler) Handle( + ctx context.Context, + query ListOrganizationsStats, +) ([]domain.OrganizationStat, error) { + stats, err := h.repo.ListStats(ctx, domain.ListOrganizationsParams{ + IDs: query.IDs, + }) + if err != nil { + return nil, fmt.Errorf("listing stats: %w", err) + } + + return stats, nil +} diff --git a/internal/kurious/domain/organization.go b/internal/kurious/domain/organization.go index 29d4a31..1166629 100644 --- a/internal/kurious/domain/organization.go +++ b/internal/kurious/domain/organization.go @@ -20,3 +20,11 @@ type Organization struct { UpdatedAt time.Time DeletedAt nullable.Value[time.Time] } + +type OrganizationStat struct { + ID string + ExternalID nullable.Value[string] + Name string + + CoursesCount uint64 +} diff --git a/internal/kurious/domain/repository.go b/internal/kurious/domain/repository.go index abac13b..4d51350 100644 --- a/internal/kurious/domain/repository.go +++ b/internal/kurious/domain/repository.go @@ -12,9 +12,10 @@ type ListCoursesParams struct { CourseThematic string OrganizationID string - NextPageToken string - Limit int - Offset int + Limit int + Offset int + OrderBy string + Ascending bool } type CreateCourseParams struct { @@ -101,9 +102,14 @@ type CreateOrganizationParams struct { LogoLink string } +type ListOrganizationsParams struct { + IDs []string +} + //go:generate mockery --name OrganizationRepository type OrganizationRepository interface { - List(context.Context) ([]Organization, error) + ListStats(context.Context, ListOrganizationsParams) ([]OrganizationStat, error) + List(context.Context, ListOrganizationsParams) ([]Organization, error) Get(context.Context, GetOrganizationParams) (Organization, error) Create(context.Context, CreateOrganizationParams) (Organization, error) Delete(ctx context.Context, id string) error @@ -111,7 +117,14 @@ type OrganizationRepository interface { type NotImplementedOrganizationRepository struct{} -func (NotImplementedOrganizationRepository) List(context.Context) ([]Organization, error) { +func (NotImplementedOrganizationRepository) ListStats( + context.Context, + ListOrganizationsParams, +) ([]OrganizationStat, error) { + return nil, ErrNotImplemented +} + +func (NotImplementedOrganizationRepository) List(context.Context, ListOrganizationsParams) ([]Organization, error) { return nil, ErrNotImplemented } func (NotImplementedOrganizationRepository) Get(context.Context, GetOrganizationParams) (Organization, error) { diff --git a/internal/kurious/ports/http/bootstrap/core.templ b/internal/kurious/ports/http/bootstrap/core.templ index e4a44c7..a932cbd 100644 --- a/internal/kurious/ports/http/bootstrap/core.templ +++ b/internal/kurious/ports/http/bootstrap/core.templ @@ -72,13 +72,113 @@ templ headerNavbar(page PageKind) { } templ footer() { - ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -201,6 +336,69 @@ func footer() templ.Component { }) } +func elementScriptsLoad() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_elementScriptsLoad_873d`, + Function: `function __templ_elementScriptsLoad_873d(){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; + }; + + const filterBySchool = () => { + const school_selector = document.getElementById('schoolSelect'); + const school_id = (school_selector !== null && school_selector.value !== '') ? school_selector.value : ''; + + const order_by = document.getElementById('sortBySelect'); + const order_by_value = (order_by !== null && order_by.value !== '') ? order_by.value : ''; + + const ascending = document.getElementById('sortByOrder'); + + const baseUrl = ` + "`" + `${window.location.pathname}?` + "`" + `; + + params = []; + if (school_id) { + params.push(` + "`" + `school_id=${school_id}` + "`" + `); + }; + if (order_by_value) { + params.push(` + "`" + `order_by=${order_by_value}` + "`" + `); + }; + if (ascending && ascending.checked) { + params.push(` + "`" + `asc=true` + "`" + `); + }; + + const finalUrl = baseUrl + params.join("&"); + if (history.pushState) { + // history.pushState(null, null, finalUrl); + // TODO: remove once htmx implemented + window.location.assign(finalUrl); + } else { + window.location.assign(finalUrl); + }; + } + + document.addEventListener('DOMContentLoaded', () => { + const ff = document.getElementById('filter-form'); + if (ff) ff.addEventListener('submit', formFilterOnSubmit); + + const fs = document.getElementById('schoolSelect'); + if (fs) fs.onchange = filterBySchool; + + const ob = document.getElementById('sortBySelect'); + if (ob) ob.onchange = filterBySchool; + });}`, + Call: templ.SafeScript(`__templ_elementScriptsLoad_873d`), + CallInline: templ.SafeScriptInline(`__templ_elementScriptsLoad_873d`), + } +} + func root(page PageKind, _ 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) @@ -209,9 +407,9 @@ func root(page PageKind, _ stats) templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var14 := templ.GetChildren(ctx) - if templ_7745c5c3_Var14 == nil { - templ_7745c5c3_Var14 = templ.NopComponent + templ_7745c5c3_Var29 := templ.GetChildren(ctx) + if templ_7745c5c3_Var29 == nil { + templ_7745c5c3_Var29 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") @@ -234,7 +432,7 @@ func root(page PageKind, _ stats) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ_7745c5c3_Var14.Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = templ_7745c5c3_Var29.Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -242,11 +440,19 @@ func root(page PageKind, _ stats) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = breadcrumbsLoad().Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = elementScriptsLoad().Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + _, 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 = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/kurious/ports/http/bootstrap/list.templ b/internal/kurious/ports/http/bootstrap/list.templ index a51faed..0eb62ee 100644 --- a/internal/kurious/ports/http/bootstrap/list.templ +++ b/internal/kurious/ports/http/bootstrap/list.templ @@ -3,27 +3,6 @@ package bootstrap import "path" import "strconv" -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 breadcrumbsItem(text, link string, isActive bool) {
  • if link != "" { @@ -78,7 +57,12 @@ templ listCoursesSectionHeader(params BreadcrumbsParams) { templ listCoursesSectionFilters(params FilterFormParams) {
    -
    +
    Filter courses + @selectOptionFromPairs("Pick a school:", params.SelectedSchoolID, params.Schools) + +
    + +
    +
    + + + +
    +
    +
    +
    +
    Promocodes
    +
    +
    +} + +templ listCoursesLearning(courses []CourseInfo) { +
    +
    + for _, course := range courses { + @listCoursesCard(course) + } +
    +
    +} + +templ listCoursesLearningLegacy(containers []CategoryContainer) { for _, container := range containers {

    { container.Name }

    for _, subcategory := range container.Subcategories { - @listCoursesThematicRow(container.ID, subcategory) + @listCoursesThematicRowLegacy(container.ID, subcategory) }
    } } -templ listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer) { +templ listCoursesThematicRowLegacy(categoryID string, subcategory SubcategoryContainer) {

    @@ -152,7 +210,7 @@ css cardTextSize() { templ listCoursesCard(info CourseInfo) { //
    -
    +
    Course picture
    @@ -175,7 +233,7 @@ templ ListCourses(pageType PageKind, s stats, params ListCoursesParams) { @root(pageType, s) { @listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams) @listCoursesSectionFilters(params.FilterForm) - @listCoursesLearning(params.Categories) + @listCoursesLearning(params.Courses) @pagination(params.Pagination) } } diff --git a/internal/kurious/ports/http/bootstrap/list_templ.go b/internal/kurious/ports/http/bootstrap/list_templ.go index 4178417..dc5c707 100644 --- a/internal/kurious/ports/http/bootstrap/list_templ.go +++ b/internal/kurious/ports/http/bootstrap/list_templ.go @@ -14,32 +14,6 @@ import "strings" import "path" import "strconv" -func breadcrumbsLoad() templ.ComponentScript { - return templ.ComponentScript{ - 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 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_e656`), - CallInline: templ.SafeScriptInline(`__templ_breadcrumbsLoad_e656`), - } -} - func breadcrumbsItem(text, link string, isActive bool) 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) @@ -87,7 +61,7 @@ func breadcrumbsItem(text, link string, isActive bool) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 33, Col: 10} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 12, Col: 10} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -105,7 +79,7 @@ func breadcrumbsItem(text, link string, isActive bool) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 35, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 14, Col: 47} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -247,12 +221,32 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var10 := `Filter courses` - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) + var templ_7745c5c3_Var10 = []any{ + templ.KV("visually-hidden", !params.Render), + "p-2", + } + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...) + 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_Var11 := `Filter courses` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -260,8 +254,8 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 = []any{"form-select"} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) + var templ_7745c5c3_Var12 = []any{"form-select"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -269,7 +263,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var11).String())) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var12).String())) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -287,8 +281,8 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var12 := `All` - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12) + templ_7745c5c3_Var13 := `All` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -319,12 +313,12 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(learningType.Name) + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, 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: 92, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 76, Col: 26} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -337,8 +331,8 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 = []any{"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...) + var templ_7745c5c3_Var15 = []any{"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -346,7 +340,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var14).String())) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var15).String())) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -364,8 +358,8 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var15 := `All` - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15) + templ_7745c5c3_Var16 := `All` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -396,12 +390,12 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.Name) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, 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: 104, Col: 28} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 88, Col: 28} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -414,12 +408,20 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var17 := `Go` - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) + templ_7745c5c3_Var18 := `Go` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = filterCoursesSelectView(params.Schools).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 } @@ -430,7 +432,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component { }) } -func listCoursesLearning(containers []CategoryContainer) templ.Component { +func selectOptionFromPairs(emptyName, activeID string, items []NameIDPair) 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 { @@ -438,22 +440,217 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var18 := templ.GetChildren(ctx) - if templ_7745c5c3_Var18 == nil { - templ_7745c5c3_Var18 = templ.NopComponent + templ_7745c5c3_Var19 := templ.GetChildren(ctx) + if templ_7745c5c3_Var19 == nil { + templ_7745c5c3_Var19 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - for _, container := range containers { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, item := range items { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, 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/list.templ`, Line: 107, Col: 14} + } + _, 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("") + 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 filterCoursesSelectView(params CoursesFilterViewParams) 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_Var22 := templ.GetChildren(ctx) + if templ_7745c5c3_Var22 == nil { + templ_7745c5c3_Var22 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var25 := `Promocodes` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) + 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 listCoursesLearning(courses []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 + } + for _, course := range courses { + templ_7745c5c3_Err = listCoursesCard(course).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 listCoursesLearningLegacy(containers []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_Var27 := templ.GetChildren(ctx) + if templ_7745c5c3_Var27 == nil { + templ_7745c5c3_Var27 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + for _, container := range containers { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var28 string + templ_7745c5c3_Var28, 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: 174, Col: 37} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -462,7 +659,7 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component { return templ_7745c5c3_Err } for _, subcategory := range container.Subcategories { - templ_7745c5c3_Err = listCoursesThematicRow(container.ID, subcategory).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = listCoursesThematicRowLegacy(container.ID, subcategory).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -479,7 +676,7 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component { }) } -func listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer) templ.Component { +func listCoursesThematicRowLegacy(categoryID string, subcategory SubcategoryContainer) 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 { @@ -487,17 +684,17 @@ func listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var20 := templ.GetChildren(ctx) - if templ_7745c5c3_Var20 == nil { - templ_7745c5c3_Var20 = templ.NopComponent + templ_7745c5c3_Var29 := templ.GetChildren(ctx) + if templ_7745c5c3_Var29 == nil { + templ_7745c5c3_Var29 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var23 := `В категогрии ` - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) + templ_7745c5c3_Var32 := `В категогрии ` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, 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: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 192, Col: 71} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -536,17 +733,17 @@ func listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var25 := `собраны ` - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) + templ_7745c5c3_Var34 := `собраны ` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(subcategory.Count)) + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, 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: 122} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 192, Col: 122} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -554,8 +751,8 @@ func listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var27 := `курсов. Раз в неделю мы обновляем информацию о всех курсах.` - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27) + templ_7745c5c3_Var36 := `курсов. Раз в неделю мы обновляем информацию о всех курсах.` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -609,17 +806,25 @@ func listCoursesCard(info CourseInfo) templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var28 := templ.GetChildren(ctx) - if templ_7745c5c3_Var28 == nil { - templ_7745c5c3_Var28 = templ.NopComponent + templ_7745c5c3_Var37 := templ.GetChildren(ctx) + if templ_7745c5c3_Var37 == nil { + templ_7745c5c3_Var37 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

    ") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var38 = []any{"card-img-top"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var38...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -635,7 +840,7 @@ func listCoursesCard(info CourseInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var29).String())) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var38).String())) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -643,8 +848,8 @@ func listCoursesCard(info CourseInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var30 = []any{"card-body", cardTextSize(), "row"} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var30...) + var templ_7745c5c3_Var39 = []any{"card-body", cardTextSize(), "row"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var39...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -652,7 +857,7 @@ func listCoursesCard(info CourseInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var30).String())) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var39).String())) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -660,12 +865,12 @@ func listCoursesCard(info CourseInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name) + var templ_7745c5c3_Var40 string + templ_7745c5c3_Var40, 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: 158, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 216, Col: 38} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -673,8 +878,8 @@ func listCoursesCard(info CourseInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var32 templ.SafeURL = templ.URL(info.OriginLink) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var32))) + var templ_7745c5c3_Var41 templ.SafeURL = templ.URL(info.OriginLink) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var41))) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -682,8 +887,8 @@ func listCoursesCard(info CourseInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var33 := `Go!` - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) + templ_7745c5c3_Var42 := `Go!` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -691,12 +896,12 @@ func listCoursesCard(info CourseInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var34 string - templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice)) + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, 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: 165, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 223, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -704,8 +909,8 @@ func listCoursesCard(info CourseInfo) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var35 := `rub.` - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35) + templ_7745c5c3_Var44 := `rub.` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var44) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -728,12 +933,12 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var36 := templ.GetChildren(ctx) - if templ_7745c5c3_Var36 == nil { - templ_7745c5c3_Var36 = templ.NopComponent + templ_7745c5c3_Var45 := templ.GetChildren(ctx) + if templ_7745c5c3_Var45 == nil { + templ_7745c5c3_Var45 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var37 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var46 := 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() @@ -755,7 +960,7 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = listCoursesLearning(params.Categories).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = listCoursesLearning(params.Courses).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -772,7 +977,7 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com } return templ_7745c5c3_Err }) - templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var37), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var46), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/kurious/ports/http/bootstrap/main.templ b/internal/kurious/ports/http/bootstrap/main.templ index 3e4d576..1bbc2d6 100644 --- a/internal/kurious/ports/http/bootstrap/main.templ +++ b/internal/kurious/ports/http/bootstrap/main.templ @@ -63,13 +63,13 @@ type Pagination struct { } templ pagination(p Pagination) { - if p.Page > 0 { + if p.Page > 0 && p.TotalPages > 0 {