rework list courses page to be flatten

This commit is contained in:
Aleksandr Trushkin
2024-04-19 00:13:03 +03:00
parent 035e9c848f
commit 3a9e01a683
34 changed files with 1803 additions and 474 deletions

View File

@ -1 +1 @@
8d0ebfae489db7ae534fee52ed6ceb3f c2a78909343b26a169423b19ada40fad

View File

@ -29,7 +29,13 @@ func main() {
} }
} }
const savingOrganizationIDInternalInsteadOfExternal = false
func app(ctx context.Context) error { func app(ctx context.Context) error {
if !savingOrganizationIDInternalInsteadOfExternal {
panic("fix saving ogranization id as external id instead of internal")
}
var cfgpath string var cfgpath string
if len(os.Args) > 1 { if len(os.Args) > 1 {
cfgpath = os.Args[1] cfgpath = os.Args[1]

View File

@ -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 { func middlewareTrace() mux.MiddlewareFunc {
reqidAttr := attribute.Key("http.request_id") methodAttr := attributeStringKey("http.request.method")
statusAttr := attribute.Key("http.status_code") reqidAttr := attributeStringKey("http.request_id")
payloadAttr := attribute.Key("http.payload_size") routeAttr := attributeStringKey("http.route")
pathAttr := attribute.Key("http.template_path") 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 func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -120,14 +130,18 @@ func middlewareTrace() mux.MiddlewareFunc {
var span trace.Span var span trace.Span
route := mux.CurrentRoute(r) route := mux.CurrentRoute(r)
hname := route.GetName()
hpath, _ := route.GetPathTemplate() hpath, _ := route.GetPathTemplate()
ctx, span = webtracer.Start( ctx, span = webtracer.Start(
ctx, "http."+hname, ctx, r.Method+" "+hpath,
trace.WithAttributes( trace.WithAttributes(
reqidAttr.String(reqid), methodAttr.Value(r.Method),
pathAttr.String(hpath), 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() defer span.End()

View File

@ -8,18 +8,19 @@ import (
"time" "time"
"git.loyso.art/frx/kurious/internal/common/config" "git.loyso.art/frx/kurious/internal/common/config"
"git.loyso.art/frx/kurious/pkg/xdefault"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel" "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/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
) )
var webtracer = otel.Tracer("kuriweb.http") var webtracer = otel.Tracer("kuriweb.http")
@ -46,19 +47,26 @@ func setupOtelSDK(ctx context.Context, cfg config.Trace) (shutdown shutdownFunc,
prop := newPropagator() prop := newPropagator()
otel.SetTextMapPropagator(prop) 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 { if err != nil {
return nil, handleError(err) return nil, handleError(err)
} }
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider) otel.SetTracerProvider(tracerProvider)
if cfg.ShowMetrics {
meterProvider, err := newMeterProvider() meterProvider, err := newMeterProvider()
if err != nil { if err != nil {
return nil, handleError(err) return nil, handleError(err)
} }
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
otel.SetMeterProvider(meterProvider) otel.SetMeterProvider(meterProvider)
}
return shutdown, nil 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) { func newCommonTraceProvider(ctx context.Context, params TraceProviderParams) (tp *trace.TracerProvider, err error) {
opts := make([]trace.TracerProviderOption, 0, 2) 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 = append(
opts, opts,
trace.WithSampler(trace.AlwaysSample()), trace.WithSampler(trace.AlwaysSample()),
trace.WithResource(resource.Default()), trace.WithResource(r),
) )
if licensekey != "" { if params.Type != config.TraceClientTypeUnset {
endpoint = xdefault.WithFallback(endpoint, defaultNewRelicEndpoint) var spanExporter trace.SpanExporter
client, err := otlptracegrpc.New( var headers map[string]string
if params.AuthHeader != "" {
headers = make(map[string]string, 1)
headers[params.AuthHeader] = params.APIKey
}
switch params.Type {
case config.TraceClientTypeGRPC:
spanExporter, err = otlptracegrpc.New(
ctx, ctx,
otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithEndpointURL(params.Endpoint),
otlptracegrpc.WithHeaders(map[string]string{ otlptracegrpc.WithHeaders(headers),
"api-key": licensekey,
}),
) )
if err != nil { case config.TraceClientTypeHTTP:
return nil, fmt.Errorf("making grpc client: %w", err) httpClient := otlptracehttp.NewClient(
} otlptracehttp.WithEndpointURL(params.Endpoint),
otlptracehttp.WithHeaders(headers),
opts = append(opts, trace.WithBatcher(client, trace.WithBatchTimeout(time.Second*10)))
} else {
traceExporter, err := stdouttrace.New(
stdouttrace.WithPrettyPrint())
if err != nil {
return nil, err
}
opts = append(
opts,
trace.WithBatcher(traceExporter, trace.WithBatchTimeout(time.Second*5)),
) )
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, fmt.Errorf("making trace exporter: %w", err)
} }
traceProvider = trace.NewTracerProvider(opts...) opts = append(opts, trace.WithBatcher(spanExporter))
}
return traceProvider, nil tp = trace.NewTracerProvider(opts...)
return tp, nil
} }
func newMeterProvider() (*metric.MeterProvider, error) { 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 { func muxHandleFunc(router *mux.Router, name, path string, hf http.HandlerFunc) *mux.Route {
h := otelhttp.WithRouteTag(path, hf) // h := otelhttp.WithRouteTag(path, hf)
return router.Handle(path, h).Name(name) return router.Handle(path, hf).Name(name)
} }

26
go.mod
View File

@ -12,15 +12,16 @@ require (
github.com/teris-io/cli v1.0.1 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-sdk/v3 v3.54.2
github.com/ydb-platform/ydb-go-yc v0.12.1 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.25.0
go.opentelemetry.io/otel v1.24.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/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/stdoutmetric v1.24.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace 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/sdk/metric v1.24.0
go.opentelemetry.io/otel/trace v1.24.0 go.opentelemetry.io/otel/trace v1.25.0
golang.org/x/net v0.22.0 golang.org/x/net v0.24.0
golang.org/x/sync v0.6.0 golang.org/x/sync v0.6.0
golang.org/x/time v0.5.0 golang.org/x/time v0.5.0
modernc.org/sqlite v1.29.3 modernc.org/sqlite v1.29.3
@ -30,11 +31,9 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.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/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // 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/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // 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/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-genproto v0.0.0-20231012155159-f85a672542fd // indirect
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // 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.25.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/sys v0.19.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.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/api v0.0.0-20240415180920-8c6c420018be // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
google.golang.org/grpc v1.62.1 // indirect google.golang.org/grpc v1.63.2 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect

52
go.sum
View File

@ -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.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.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= 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.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/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= 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.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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.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.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/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= 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.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 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/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc=
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/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= 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/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 h1:JYE2HM7pZbOt5Jhk8ndWZTUWYOVift2cHjXVMkPdmdc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0/go.mod h1:yMb/8c6hVsnma0RpsBMNo0fEiQKeclawtgaIaOp2MLY= 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 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= 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.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= 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 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8=
go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0= 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.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 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.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.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 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.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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= 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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 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-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-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 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-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU=
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c= 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-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= 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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 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.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.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.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= 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/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-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

View File

@ -1,34 +1,53 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Test page</title> <title>Test page</title>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" <link
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" /> href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script> crossorigin="anonymous"
></script>
<link rel="stylesheet" href="/assets/style.css" /> <link rel="stylesheet" href="/assets/style.css" />
</head> </head>
<body data-bs-theme="dark" style="margin: 0"> <body data-bs-theme="dark">
<header> <header>
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto"> <nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/index.html">Kurious</a> <a class="navbar-brand" href="/index.html">Kurious</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" <button
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="navbar-collapse collapse" id="navbarSupportedContent"> <div class="navbar-collapse collapse" id="navbarSupportedContent">
<ul class="navbar-nav mb-lg-0 mb-2 me-auto"> <ul class="navbar-nav mb-lg-0 mb-2 me-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" aria-current="page" href="/index.html">Home</a> <a class="nav-link" aria-current="page" href="/index.html"
>Home</a
>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="/courses.html">Courses</a> <a
class="nav-link active"
aria-current="page"
href="/courses.html"
>Courses</a
>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/core.html">About us</a> <a class="nav-link" href="/core.html">About us</a>
@ -41,7 +60,11 @@
<div class="container"> <div class="container">
<section class="row header"> <section class="row header">
<nav class="mt-4" style="--bs-breadcrumb-divider: '/'" aria-label="breadcrumb"> <nav
class="mt-4"
style="--bs-breadcrumb-divider: &quot;>&quot;"
aria-label="breadcrumb"
>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#">Main</a></li> <li class="breadcrumb-item"><a href="#">Main</a></li>
<li class="breadcrumb-item" aria-current="page"> <li class="breadcrumb-item" aria-current="page">
@ -57,14 +80,22 @@
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Filter categories</span> <span class="input-group-text">Filter categories</span>
<select class="form-select" id="inputGroupSelect04" aria-label="Example select with button addon"> <select
class="form-select"
id="inputGroupSelect04"
aria-label="Example select with button addon"
>
<option selected>All</option> <option selected>All</option>
<option value="1">Programming</option> <option value="1">Programming</option>
<option value="2">Design</option> <option value="2">Design</option>
<option value="3">Business</option> <option value="3">Business</option>
</select> </select>
<select class="form-select" id="inputGroupSelect04" aria-label="Example select with button addon"> <select
class="form-select"
id="inputGroupSelect04"
aria-label="Example select with button addon"
>
<option selected>All</option> <option selected>All</option>
<option value="1">Web development</option> <option value="1">Web development</option>
<option value="2">Backend</option> <option value="2">Backend</option>
@ -77,50 +108,107 @@
</div> </div>
</section> </section>
<section class="row first-class-group">
<h1 class="title">Languages</h1>
<p>A languages category provides all courses to help learn language</p>
<div class="filter-content d-flex mb-3"> <div class="filter-content d-flex mb-3">
<div class="p-2"> <!-- School list -->
<select class="form-select" id="inputGroupSelect04" aria-label="Example select with button addon"> <div class="p-2 col-auto">
<select
class="form-select"
id="inputGroupSelect04"
aria-label="Example select with button addon"
>
<option selected>Pick a school</option> <option selected>Pick a school</option>
<option value="1">First school in the row</option> <option value="1">First school in the row</option>
<option value="2"> <option value="2">Second but not the shortest named school</option>
Second but not the shortest named school
</option>
<option value="3">Third small</option> <option value="3">Third small</option>
</select> </select>
</div> </div>
<!-- Sort option -->
<div class="p-2"> <div class="col-auto">
<select class="form-select" id="inputGroupSelect04" aria-label="Example select with button addon"> <div class="input-group flex-nowrap p-2">
<select
class="form-select"
id="inputGroupSelect04"
aria-label="Example select with button addon"
>
<option selected>Sort by</option> <option selected>Sort by</option>
<option value="1">One</option> <option value="1">One</option>
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
<option value="4">Threerrrrrrrrrrrrrrrrrr</option> <option value="4">Threerrrrrrrrrrrrrrrrrr</option>
</select> </select>
</div>
<div class="ms-auto p-2"> <input
type="radio"
class="btn-check p-2"
name="options-base"
id="option6"
autocomplete="off"
checked
/>
<label class="btn" for="option6">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-sort-down"
viewBox="0 0 16 16"
>
<path
d="M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"
/>
</svg>
</label>
<input
type="radio"
class="btn-check p-2"
name="options-base"
id="option5"
autocomplete="off"
checked
/>
<label class="btn" for="option5">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-sort-up"
viewBox="0 0 16 16"
>
<path
d="M3.5 12.5a.5.5 0 0 1-1 0V3.707L1.354 4.854a.5.5 0 1 1-.708-.708l2-1.999.007-.007a.5.5 0 0 1 .7.006l2 2a.5.5 0 1 1-.707.708L3.5 3.707zm3.5-9a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"
/>
</svg>
</label>
</div>
</div>
<!-- Spacer -->
<div class="col-4"></div>
<!-- Promocodes button -->
<div class="col-auto ms-auto p-2">
<div class="btn btn-primary">Promocodes</div> <div class="btn btn-primary">Promocodes</div>
</div> </div>
</div> </div>
<div class="second-class-group block">
<h2 class="title">Japanese</h2>
<p>Looking for a course to learn japanese language?</p>
<div class="row g-4"> <div class="row g-4">
<div class="col-12 col-md-6 col-lg-3"> <div class="col-12 col-md-6 col-lg-3">
<div class="card"> <div class="card">
<img src="https://placehold.co/128x128" class="card-img-top" alt="..." /> <img
src="https://placehold.co/128x128"
class="card-img-top"
alt="..."
/>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Card title with a long naming</h5> <h5 class="card-title">Card title with a long naming</h5>
<div class="input-group d-flex"> <div class="input-group d-flex">
<a href="#" class="btn text btn-outline-primary flex-grow-1">Open ></a> <a href="#" class="btn text btn-outline-primary flex-grow-1"
<span class="input-group-text justify-content-end flex-fill">500$</span> >Open ></a
>
<span class="input-group-text justify-content-end flex-fill"
>500$</span
>
</div> </div>
</div> </div>
</div> </div>
@ -128,15 +216,62 @@
</div> </div>
</div> </div>
<hr /> <!-- Maybe add spacing in a better way? -->
<p class="p-2"></p>
<footer class="text-center text-lg-start bg-body-tertiary text-muted">
<section class="p-2">
<div class="container text-center text-md-start mt-4">
<div class="row mt-3">
<div class="col-md-3 col-lg-4 col-xl-3 mx-auto mb-4">
<h6 class="text-uppercase fw-bold mb-4">
<i class="fas fa-gem me-3"></i>Courses
</h6>
<p>
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.
</p>
</div>
<div class="col-md-3 col-lg-2 col-xl-2 mx-auto mb-4">
<h6 class="text-uppercase fw-bold mb-4">Useful links</h6>
<p>
<a href="#!" class="text-reset">Pricing</a>
</p>
<p>
<a href="#!" class="text-reset">Settings</a>
</p>
<p>
<a href="#!" class="text-reset">Orders</a>
</p>
<p>
<a href="#!" class="text-reset">Help</a>
</p>
</div>
<div class="col-md-4 col-lg-3 col-xl-3 mx-auto mb-md-0 mb-4">
<h6 class="text-uppercase fw-bold mb-4">Contact</h6>
<p><i class="fas fa-home me-3"></i> New York, NY 10012, US</p>
<p>
<i class="fas fa-envelope me-3"></i>
info@example.com
</p>
<p><i class="fas fa-phone me-3"></i> + 01 234 567 88</p>
<p><i class="fas fa-print me-3"></i> + 01 234 567 89</p>
</div>
</div>
</div>
</section> </section>
<footer class="row"> <div
<div class="text-end"> class="text-center p-4"
<p>(c) All right reserved</p> style="background-color: rgba(0, 0, 0, 0.05)"
>
© 2024 Copyright:
<a class="text-reset fw-bold" href="https://mdbootstrap.com/"
>kursov.net</a
>
</div> </div>
</footer> </footer>
</div>
</body> </body>
</html> </html>

View File

@ -1,6 +1,38 @@
package config package config
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 { type Trace struct {
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
LicenseKey string `json:"license_key"` APIKey string `json:"api_key"`
APIHeader string `json:"api_header"`
Type TraceClientType `json:"type"`
ShowMetrics bool `json:"show_metrics"`
} }

View File

@ -16,11 +16,10 @@ import (
) )
var ( var (
commandAttribute = attribute.Key("command_name") nameAttribute = attribute.Key("cq.name")
queryAttribute = attribute.Key("query_name") argsAttribute = attribute.Key("cq.args")
argsAttribute = attribute.Key("args")
apiTracer = otel.Tracer("cq") apiTracer = otel.Tracer("api")
) )
type commandLoggingDecorator[T any] struct { 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) _ = json.NewEncoder(&argsBuilder).Encode(cmd)
var span trace.Span var span trace.Span
ctx, span = apiTracer.Start(ctx, handlerName) ctx, span = apiTracer.Start(ctx, "command "+handlerName)
span.SetAttributes( span.SetAttributes(
commandAttribute.String(handlerName), nameAttribute.String(handlerName),
argsAttribute.String(argsBuilder.String()), 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) _ = json.NewEncoder(&argsBuilder).Encode(query)
var span trace.Span var span trace.Span
ctx, span = apiTracer.Start(ctx, handlerName) ctx, span = apiTracer.Start(ctx, "query "+handlerName)
span.SetAttributes( span.SetAttributes(
queryAttribute.String(handlerName), nameAttribute.String(handlerName),
argsAttribute.String(argsBuilder.String()), argsAttribute.String(argsBuilder.String()),
) )

View File

@ -1,6 +1,21 @@
package adapters 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 { type domainer[T any] interface {
AsDomain() T AsDomain() T

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/domain" "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.stats = map[string]domain.LearningTypeStat{}
m.courseThematicByLearningType = map[string]string{} m.courseThematicByLearningType = map[string]string{}
var nextPageToken string var offset int
for { for {
result, err := cr.List(ctx, domain.ListCoursesParams{ result, err := cr.List(ctx, domain.ListCoursesParams{
LearningType: "", LearningType: "",
CourseThematic: "", CourseThematic: "",
NextPageToken: nextPageToken, Limit: batchSize + 1,
Limit: batchSize, Offset: offset,
}) })
if err != nil { if err != nil {
return fmt.Errorf("listing courses: %w", err) 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, ok := m.stats[course.LearningTypeID]
stat.Count++ stat.Count++
if !ok { if !ok {
@ -50,11 +58,12 @@ func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepo
stat.CourseThematic[course.ThematicID]++ stat.CourseThematic[course.ThematicID]++
m.stats[course.LearningTypeID] = stat m.stats[course.LearningTypeID] = stat
m.courseThematicByLearningType[course.ThematicID] = course.LearningTypeID m.courseThematicByLearningType[course.ThematicID] = course.LearningTypeID
} })
if len(result.Courses) < batchSize {
if len(result.Courses) != batchSize+1 {
break break
} }
nextPageToken = result.NextPageToken offset += batchSize
} }
return nil return nil

View File

@ -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"),
}
}

View File

@ -9,52 +9,15 @@ import (
"strings" "strings"
"time" "time"
"git.loyso.art/frx/kurious/internal/common/config"
"git.loyso.art/frx/kurious/internal/common/nullable" "git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/common/xcontext" "git.loyso.art/frx/kurious/internal/common/xcontext"
"git.loyso.art/frx/kurious/internal/kurious/domain" "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" "github.com/jmoiron/sqlx"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "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 { func (c *sqliteConnection) CourseRepository() *sqliteCourseRepository {
return &sqliteCourseRepository{ return &sqliteCourseRepository{
db: c.db, db: c.db,
@ -73,7 +36,15 @@ func (r *sqliteCourseRepository) List(
) (result domain.ListCoursesResult, err error) { ) (result domain.ListCoursesResult, err error) {
const queryTemplate = `SELECT %s from courses WHERE 1=1` 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() { defer func() {
if err != nil { if err != nil {
span.RecordError(err) span.RecordError(err)
@ -81,10 +52,6 @@ func (r *sqliteCourseRepository) List(
span.End() 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) query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
args := make([]any, 0, 6) args := make([]any, 0, 6)
if params.LearningType != "" { if params.LearningType != "" {
@ -99,12 +66,16 @@ func (r *sqliteCourseRepository) List(
args = append(args, params.OrganizationID) args = append(args, params.OrganizationID)
query += " AND organization_id = ?" query += " AND organization_id = ?"
} }
if params.NextPageToken != "" {
args = append(args, params.NextPageToken) if params.OrderBy == "" {
query += " AND id > ?" 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 { if params.Limit > 0 {
query += " LIMIT ?" query += " LIMIT ?"
@ -115,9 +86,8 @@ func (r *sqliteCourseRepository) List(
args = append(args, params.Offset) args = append(args, params.Offset)
} }
span.SetAttributes( span.SetAttributes(dbStatementAttr.String(query))
attribute.String("query", query),
)
scanF := func(s rowsScanner) (err error) { scanF := func(s rowsScanner) (err error) {
var cdb sqliteCourseDB var cdb sqliteCourseDB
err = s.StructScan(&cdb) err = s.StructScan(&cdb)
@ -144,8 +114,8 @@ func (r *sqliteCourseRepository) List(
} }
span.SetAttributes( span.SetAttributes(
attribute.Int("items_count", len(result.Courses)), attribute.Int("db.items_count", len(result.Courses)),
attribute.Int("total_items", result.Count), attribute.Int("db.total_items", result.Count),
) )
return result, nil return result, nil
} }
@ -155,6 +125,23 @@ func (r *sqliteCourseRepository) ListLearningTypes(
) (result domain.ListLearningTypeResult, err error) { ) (result domain.ListLearningTypeResult, err error) {
const query = "SELECT DISTINCT learning_type FROM courses" 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) err = r.db.SelectContext(ctx, &result.LearningTypeIDs, query)
if err != nil { if err != nil {
return result, fmt.Errorf("executing query: %w", err) return result, fmt.Errorf("executing query: %w", err)
@ -176,6 +163,23 @@ func (r *sqliteCourseRepository) ListCourseThematics(
query += " AND learning_type = ?" 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...) err = r.db.SelectContext(ctx, &result.CourseThematicIDs, query, args...)
if err != nil { if err != nil {
return result, fmt.Errorf("executing query: %w", err) return result, fmt.Errorf("executing query: %w", err)
@ -191,6 +195,24 @@ func (r *sqliteCourseRepository) Get(
const queryTemplate = `SELECT %s FROM courses WHERE id = ?` const queryTemplate = `SELECT %s FROM courses WHERE id = ?`
query := fmt.Sprintf(queryTemplate, coursesFieldsStr) 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 var courseDB sqliteCourseDB
err = r.db.GetContext(ctx, &courseDB, query, id) err = r.db.GetContext(ctx, &courseDB, query, id)
if err != nil { 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) { func (r *sqliteCourseRepository) listCount(ctx context.Context, params domain.ListCoursesParams) (count int, err error) {
const queryTemplate = `SELECT COUNT(id) FROM courses WHERE 1=1` 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 query := queryTemplate
args := make([]any, 0, 6) args := make([]any, 0, 6)
@ -283,6 +297,23 @@ func (r *sqliteCourseRepository) listCount(ctx context.Context, params domain.Li
query += " AND organization_id = ?" 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...) err = r.db.GetContext(ctx, &count, query, args...)
if err != nil { if err != nil {
return count, fmt.Errorf("sending query: %w", err) 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...)
}

View File

@ -104,13 +104,4 @@ func (s *sqliteCourseRepositorySuite) TestListLimitOffset() {
result, err := cr.List(s.ctx, params) result, err := cr.List(s.ctx, params)
s.NoError(err) s.NoError(err)
s.Empty(result.Courses) 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
}
} }

View File

@ -12,6 +12,8 @@ import (
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
) )
var ( var (
@ -50,7 +52,7 @@ type sqliteLearingCategoryRepository struct {
log *slog.Logger 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)" + const queryTemplate = "INSERT INTO learning_categories (%s)" +
" VALUES (%s)" + " VALUES (%s)" +
" ON CONFLICT(id) DO UPDATE" + " 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)), ","), 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, ctx, query,
c.ID, c.ID,
nullableValueAsString(c.Logo), nullableValueAsString(c.Logo),
@ -82,6 +101,23 @@ func (r *sqliteLearingCategoryRepository) List(ctx context.Context) (out []domai
query := fmt.Sprintf(queryTemplate, learningCategoryColumnsStr) 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 var categories []learningCategoryDB
err = r.db.SelectContext(ctx, &categories, query) err = r.db.SelectContext(ctx, &categories, query)
if err != nil { if err != nil {
@ -93,13 +129,31 @@ func (r *sqliteLearingCategoryRepository) List(ctx context.Context) (out []domai
return out, nil 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 = ?;" const queryTemplate = "SELECT %s FROM learning_categories WHERE id = ?;"
query := fmt.Sprintf(queryTemplate, learningCategoryColumnsStr) 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 var cdb learningCategoryDB
err := r.db.GetContext(ctx, &cdb, query, id) err = r.db.GetContext(ctx, &cdb, query, id)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return domain.LearningCategory{}, domain.ErrNotFound return domain.LearningCategory{}, domain.ErrNotFound
@ -109,3 +163,12 @@ func (r *sqliteLearingCategoryRepository) Get(ctx context.Context, id string) (d
return cdb.AsDomain(), nil return cdb.AsDomain(), nil
} }
func (c *sqliteLearingCategoryRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue {
outbase := append(
getSqliteBaseAttributes(),
dbTableAttr.String("learning_categories"),
)
return append(outbase, custom...)
}

View File

@ -6,10 +6,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"strings"
"time" "time"
"git.loyso.art/frx/kurious/internal/common/xslices" "git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@ -31,6 +34,22 @@ var (
organizationColumnsArgsStr = namedArgColumns(organizationColumns) 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 { type organizationDB struct {
ID string `db:"id"` ID string `db:"id"`
ExternalID sql.NullString `db:"external_id"` ExternalID sql.NullString `db:"external_id"`
@ -83,10 +102,73 @@ type sqliteOrganizationRepository struct {
log *slog.Logger log *slog.Logger
} }
func (r *sqliteOrganizationRepository) List(ctx context.Context) (out []domain.Organization, err error) { func (r *sqliteOrganizationRepository) ListStats(
const queryTemplate = `SELECT %s FROM organizations` 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) 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) organizations := make([]organizationDB, 0, 1<<8)
err = r.db.SelectContext(ctx, &organizations, query) err = r.db.SelectContext(ctx, &organizations, query)
if err != nil { if err != nil {
@ -109,6 +191,23 @@ func (r *sqliteOrganizationRepository) Get(ctx context.Context, params domain.Ge
query += " AND external_id = ?" 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 var orgdb organizationDB
err = r.db.GetContext(ctx, &orgdb, query, args...) err = r.db.GetContext(ctx, &orgdb, query, args...)
if err != nil { 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` const queryTemplate = `INSERT INTO organizations (%[1]s) VALUES (%[2]s) RETURNING %[1]s`
query := fmt.Sprintf(queryTemplate, organizationColumnsStr, organizationColumnsArgsStr) 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) stmt, err := r.db.PrepareNamedContext(ctx, query)
if err != nil { if err != nil {
return out, fmt.Errorf("preparing statement: %w", err) 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 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 = ?` 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) result, err := r.db.ExecContext(ctx, query, id)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
@ -168,3 +302,12 @@ func (r *sqliteOrganizationRepository) Delete(ctx context.Context, id string) er
return nil return nil
} }
func (c *sqliteOrganizationRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue {
outbase := append(
getSqliteBaseAttributes(),
dbTableAttr.String("organizaitons"),
)
return append(outbase, custom...)
}

View File

@ -51,7 +51,7 @@ func (s *sqliteOrganzationRepositorySuite) TestList() {
orgsdb = append(orgsdb, gotOrg) 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) s.NoError(err)
compareF := func(lhs, rhs domain.Organization) int { compareF := func(lhs, rhs domain.Organization) int {

View File

@ -178,7 +178,6 @@ func (r *ydbCourseRepository) List(
opts = append( opts = append(
opts, opts,
table.ValueParam("$id", types.TextValue(params.NextPageToken)),
table.ValueParam("$limit", types.Int32Value(int32(params.Limit))), table.ValueParam("$limit", types.Int32Value(int32(params.Limit))),
) )

View File

@ -22,6 +22,7 @@ type Queries struct {
ListCourseStatistics query.ListCoursesStatsHandler ListCourseStatistics query.ListCoursesStatsHandler
ListOrganzations query.ListOrganizationsHandler ListOrganzations query.ListOrganizationsHandler
ListOrganizationsStats query.ListOrganizationsStatsHandler
GetOrganization query.GetOrganizationHandler GetOrganization query.GetOrganizationHandler
} }

View File

@ -17,6 +17,9 @@ type ListCourse struct {
OrganizationID string OrganizationID string
Keyword string Keyword string
OrderBy string
Ascending bool
Limit int Limit int
Offset int Offset int
NextPageToken string NextPageToken string
@ -57,6 +60,8 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do
OrganizationID: query.OrganizationID, OrganizationID: query.OrganizationID,
Limit: query.Limit, Limit: query.Limit,
Offset: query.Offset, Offset: query.Offset,
OrderBy: query.OrderBy,
Ascending: query.Ascending,
}) })
if err != nil { if err != nil {
return out, fmt.Errorf("listing courses: %w", err) return out, fmt.Errorf("listing courses: %w", err)

View File

@ -9,7 +9,9 @@ import (
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
) )
type ListOrganizations struct{} type ListOrganizations struct {
IDs []string
}
type ListOrganizationsHandler decorator.QueryHandler[ListOrganizations, []domain.Organization] 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) { 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 { if err != nil {
return nil, fmt.Errorf("listing organizations: %w", err) return nil, fmt.Errorf("listing organizations: %w", err)
} }

View File

@ -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
}

View File

@ -20,3 +20,11 @@ type Organization struct {
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt nullable.Value[time.Time] DeletedAt nullable.Value[time.Time]
} }
type OrganizationStat struct {
ID string
ExternalID nullable.Value[string]
Name string
CoursesCount uint64
}

View File

@ -12,9 +12,10 @@ type ListCoursesParams struct {
CourseThematic string CourseThematic string
OrganizationID string OrganizationID string
NextPageToken string
Limit int Limit int
Offset int Offset int
OrderBy string
Ascending bool
} }
type CreateCourseParams struct { type CreateCourseParams struct {
@ -101,9 +102,14 @@ type CreateOrganizationParams struct {
LogoLink string LogoLink string
} }
type ListOrganizationsParams struct {
IDs []string
}
//go:generate mockery --name OrganizationRepository //go:generate mockery --name OrganizationRepository
type OrganizationRepository interface { 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) Get(context.Context, GetOrganizationParams) (Organization, error)
Create(context.Context, CreateOrganizationParams) (Organization, error) Create(context.Context, CreateOrganizationParams) (Organization, error)
Delete(ctx context.Context, id string) error Delete(ctx context.Context, id string) error
@ -111,7 +117,14 @@ type OrganizationRepository interface {
type NotImplementedOrganizationRepository struct{} 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 return nil, ErrNotImplemented
} }
func (NotImplementedOrganizationRepository) Get(context.Context, GetOrganizationParams) (Organization, error) { func (NotImplementedOrganizationRepository) Get(context.Context, GetOrganizationParams) (Organization, error) {

View File

@ -72,13 +72,113 @@ templ headerNavbar(page PageKind) {
} }
templ footer() { templ footer() {
<footer> <footer class="text-center text-lg-start bg-body-tertiary text-muted">
<div class="row"> <section class="p-2">
<span>(c) kurious, 2024. All rights reserved.</span> <div class="container text-center text-md-start mt-5">
<div class="row mt-3">
<div class="col-md-3 col-lg-4 col-xl-3 mx-auto mb-4">
<h6 class="text-uppercase fw-bold mb-4">
<i class="fas fa-gem me-3"></i>Courses
</h6>
<p>
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.
</p>
</div>
<div class="col-md-3 col-lg-2 col-xl-2 mx-auto mb-4">
<h6 class="text-uppercase fw-bold mb-4">Useful links</h6>
<p>
<a href="#!" class="text-reset">Pricing</a>
</p>
<p>
<a href="#!" class="text-reset">Settings</a>
</p>
<p>
<a href="#!" class="text-reset">Orders</a>
</p>
<p>
<a href="#!" class="text-reset">Help</a>
</p>
</div>
<div class="col-md-4 col-lg-3 col-xl-3 mx-auto mb-md-0 mb-4">
<h6 class="text-uppercase fw-bold mb-4">Contact</h6>
<p><i class="fas fa-home me-3"></i> New York, NY 10012, US</p>
<p>
<i class="fas fa-envelope me-3"></i>
info@example.com
</p>
<p><i class="fas fa-phone me-3"></i> + 01 234 567 88</p>
<p><i class="fas fa-print me-3"></i> + 01 234 567 89</p>
</div>
</div>
</div>
</section>
<div class="text-center p-4" style="background-color: rgba(0, 0, 0, 0.05)">
© 2024 Copyright:
<a class="text-reset fw-bold" href="https://mdbootstrap.com/">kursov.net</a>
</div> </div>
</footer> </footer>
} }
script elementScriptsLoad() {
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;
});
}
templ root(page PageKind, _ stats) { templ root(page PageKind, _ stats) {
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
@ -88,7 +188,8 @@ templ root(page PageKind, _ stats) {
<div class="container"> <div class="container">
{ children... } { children... }
</div> </div>
@breadcrumbsLoad() @elementScriptsLoad()
</body> </body>
@footer()
</html> </html>
} }

View File

@ -181,16 +181,151 @@ func footer() templ.Component {
templ_7745c5c3_Var12 = templ.NopComponent templ_7745c5c3_Var12 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer><div class=\"row\"><span>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer class=\"text-center text-lg-start bg-body-tertiary text-muted\"><section class=\"p-2\"><div class=\"container text-center text-md-start mt-5\"><div class=\"row mt-3\"><div class=\"col-md-3 col-lg-4 col-xl-3 mx-auto mb-4\"><h6 class=\"text-uppercase fw-bold mb-4\"><i class=\"fas fa-gem me-3\"></i>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var13 := `(c) kurious, 2024. All rights reserved.` templ_7745c5c3_Var13 := `Courses`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></div></footer>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h6><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var14 := `Welcome to Courses, your gateway to learning! Explore a diverse`
_, 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
}
templ_7745c5c3_Var15 := `range of courses and advance your skills with us. Join our`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var16 := `community and transform your life through education.`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div><div class=\"col-md-3 col-lg-2 col-xl-2 mx-auto mb-4\"><h6 class=\"text-uppercase fw-bold mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var17 := `Useful links`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h6><p><a href=\"#!\" class=\"text-reset\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var18 := `Pricing`
_, 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("</a></p><p><a href=\"#!\" class=\"text-reset\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var19 := `Settings`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></p><p><a href=\"#!\" class=\"text-reset\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var20 := `Orders`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></p><p><a href=\"#!\" class=\"text-reset\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var21 := `Help`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></p></div><div class=\"col-md-4 col-lg-3 col-xl-3 mx-auto mb-md-0 mb-4\"><h6 class=\"text-uppercase fw-bold mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var22 := `Contact`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h6><p><i class=\"fas fa-home me-3\"></i> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var23 := `New York, NY 10012, US`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p><i class=\"fas fa-envelope me-3\"></i> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var24 := `info@example.com`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p><i class=\"fas fa-phone me-3\"></i> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var25 := `+ 01 234 567 88`
_, 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("</p><p><i class=\"fas fa-print me-3\"></i> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var26 := `+ 01 234 567 89`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div></div></section><div class=\"text-center p-4\" style=\"background-color: rgba(0, 0, 0, 0.05)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var27 := `© 2024 Copyright:`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <a class=\"text-reset fw-bold\" href=\"https://mdbootstrap.com/\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var28 := `kursov.net`
_, 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("</a></div></footer>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { func root(page PageKind, _ stats) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { 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) 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) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var14 := templ.GetChildren(ctx) templ_7745c5c3_Var29 := templ.GetChildren(ctx)
if templ_7745c5c3_Var14 == nil { if templ_7745c5c3_Var29 == nil {
templ_7745c5c3_Var14 = templ.NopComponent templ_7745c5c3_Var29 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"ru\">") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"ru\">")
@ -234,7 +432,7 @@ func root(page PageKind, _ stats) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -242,11 +440,19 @@ func root(page PageKind, _ stats) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body>")
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("</html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -3,27 +3,6 @@ package bootstrap
import "path" import "path"
import "strconv" 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) { templ breadcrumbsItem(text, link string, isActive bool) {
<li class={ "breadcrumb-item", templ.KV("active", isActive) }> <li class={ "breadcrumb-item", templ.KV("active", isActive) }>
if link != "" { if link != "" {
@ -78,7 +57,12 @@ templ listCoursesSectionHeader(params BreadcrumbsParams) {
templ listCoursesSectionFilters(params FilterFormParams) { templ listCoursesSectionFilters(params FilterFormParams) {
<section class="row filters"> <section class="row filters">
<div class="col-8"> <div
class={
templ.KV("visually-hidden", !params.Render),
"p-2",
}
>
<form id="filter-form" class="input-group"> <form id="filter-form" class="input-group">
<span class="input-group-text">Filter courses</span> <span class="input-group-text">Filter courses</span>
<select <select
@ -108,21 +92,95 @@ templ listCoursesSectionFilters(params FilterFormParams) {
<button id="filter-course-thematic" class="btn btn-outline-secondary" type="submit">Go</button> <button id="filter-course-thematic" class="btn btn-outline-secondary" type="submit">Go</button>
</form> </form>
</div> </div>
@filterCoursesSelectView(params.Schools)
</section> </section>
} }
templ listCoursesLearning(containers []CategoryContainer) { templ selectOptionFromPairs(emptyName, activeID string, items []NameIDPair) {
<option value="" selected?={ activeID == "" }>
{ emptyName }
</option>
for _, item := range items {
<option
selected?={ activeID==item.ID }
value={ item.ID }
>
{ item.Name }
</option>
}
}
templ filterCoursesSelectView(params CoursesFilterViewParams) {
<div class="filter-content d-flex p-2">
<!-- School list -->
<div class="col-3">
<select name="school" id="schoolSelect" class="form-select" aria-label="school filter">
@selectOptionFromPairs("Pick a school:", params.SelectedSchoolID, params.Schools)
</select>
</div>
<!-- Sort options -->
<div class="col-3 mx-4">
<div class="input-group flex-nowrap">
<select
class="form-select"
id="sortBySelect"
aria-label="Sorting items by requested order"
>
@selectOptionFromPairs("Sort by:", params.OrderBy, params.OrderFields)
</select>
<input
type="radio"
class="btn-check"
name="sortByOrder"
id="sortByOrder"
autocomplete="off"
checked?={ !params.Ascending }
/>
<label class="btn" for="sortByOrder">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-sort-down"
viewBox="0 0 16 16"
>
<path
d="M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"
></path>
</svg>
</label>
</div>
</div>
<div class="col-4"></div>
<div class="col-auto ms-auto">
<div class="btn btn-primary">Promocodes</div>
</div>
</div>
}
templ listCoursesLearning(courses []CourseInfo) {
<div class="block">
<div class="row row-cols-1 row-cols-md-4 g-4">
for _, course := range courses {
@listCoursesCard(course)
}
</div>
</div>
}
templ listCoursesLearningLegacy(containers []CategoryContainer) {
for _, container := range containers { for _, container := range containers {
<section class="row first-class-group g-4"> <section class="row first-class-group g-4">
<h1 class="title">{ container.Name }</h1> <h1 class="title">{ container.Name }</h1>
for _, subcategory := range container.Subcategories { for _, subcategory := range container.Subcategories {
@listCoursesThematicRow(container.ID, subcategory) @listCoursesThematicRowLegacy(container.ID, subcategory)
} }
</section> </section>
} }
} }
templ listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer) { templ listCoursesThematicRowLegacy(categoryID string, subcategory SubcategoryContainer) {
<div class="block second-class-group"> <div class="block second-class-group">
<h3 class="title"> <h3 class="title">
<a href={ templ.SafeURL("/courses/" + categoryID + "/" + subcategory.ID) } class="text-decoration-none"> <a href={ templ.SafeURL("/courses/" + categoryID + "/" + subcategory.ID) } class="text-decoration-none">
@ -152,7 +210,7 @@ css cardTextSize() {
templ listCoursesCard(info CourseInfo) { templ listCoursesCard(info CourseInfo) {
// <div class="col-12 col-md-6 col-lg-3"> // <div class="col-12 col-md-6 col-lg-3">
<div class="col"> <div class="col" id={ info.ID }>
<div class="card h-100"> <div class="card h-100">
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128") } alt="Course picture" class={ "card-img-top" }/> <img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128") } alt="Course picture" class={ "card-img-top" }/>
<div class={ "card-body", cardTextSize(), "row" }> <div class={ "card-body", cardTextSize(), "row" }>
@ -175,7 +233,7 @@ templ ListCourses(pageType PageKind, s stats, params ListCoursesParams) {
@root(pageType, s) { @root(pageType, s) {
@listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams) @listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams)
@listCoursesSectionFilters(params.FilterForm) @listCoursesSectionFilters(params.FilterForm)
@listCoursesLearning(params.Categories) @listCoursesLearning(params.Courses)
@pagination(params.Pagination) @pagination(params.Pagination)
} }
} }

View File

@ -14,32 +14,6 @@ import "strings"
import "path" import "path"
import "strconv" 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 { 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) { 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) 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 var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -105,7 +79,7 @@ func breadcrumbsItem(text, link string, isActive bool) templ.Component {
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -247,12 +221,32 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
templ_7745c5c3_Var9 = templ.NopComponent templ_7745c5c3_Var9 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"row filters\"><div class=\"col-8\"><form id=\"filter-form\" class=\"input-group\"><span class=\"input-group-text\">") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"row filters\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var10 := `Filter courses` var templ_7745c5c3_Var10 = []any{
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) 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("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var10).String()))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><form id=\"filter-form\" class=\"input-group\"><span class=\"input-group-text\">")
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -260,8 +254,8 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 = []any{"form-select"} var templ_7745c5c3_Var12 = []any{"form-select"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -269,7 +263,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -287,8 +281,8 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var12 := `All` templ_7745c5c3_Var13 := `All`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -319,12 +313,12 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(learningType.Name) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(learningType.Name)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -337,8 +331,8 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 = []any{"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)} 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_Var14...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -346,7 +340,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -364,8 +358,8 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var15 := `All` templ_7745c5c3_Var16 := `All`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -396,12 +390,12 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.Name) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.Name)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -414,12 +408,20 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var17 := `Go` templ_7745c5c3_Var18 := `Go`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button></form></div></section>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button></form></div>")
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("</section>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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) { 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) templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer { if !templ_7745c5c3_IsBuffer {
@ -438,22 +440,217 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component {
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var18 := templ.GetChildren(ctx) templ_7745c5c3_Var19 := templ.GetChildren(ctx)
if templ_7745c5c3_Var18 == nil { if templ_7745c5c3_Var19 == nil {
templ_7745c5c3_Var18 = templ.NopComponent templ_7745c5c3_Var19 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
for _, container := range containers { _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option value=\"\"")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"row first-class-group\"><h1 class=\"title\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var19 string if activeID == "" {
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(container.Name) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 116, Col: 37} return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(emptyName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 100, Col: 13}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range items {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if activeID == item.ID {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(item.ID))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
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("</option>")
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("<div class=\"filter-content d-flex p-2\"><!--")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var23 := ` School list `
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("--><div class=\"col-3\"><select name=\"school\" id=\"schoolSelect\" class=\"form-select\" aria-label=\"school filter\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = selectOptionFromPairs("Pick a school:", params.SelectedSchoolID, params.Schools).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select></div><!--")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var24 := ` Sort options `
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("--><div class=\"col-3 mx-4\"><div class=\"input-group flex-nowrap\"><select class=\"form-select\" id=\"sortBySelect\" aria-label=\"Sorting items by requested order\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = selectOptionFromPairs("Sort by:", params.OrderBy, params.OrderFields).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> <input type=\"radio\" class=\"btn-check\" name=\"sortByOrder\" id=\"sortByOrder\" autocomplete=\"off\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !params.Ascending {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" checked")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("> <label class=\"btn\" for=\"sortByOrder\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-sort-down\" viewBox=\"0 0 16 16\"><path d=\"M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z\"></path></svg></label></div></div><div class=\"col-4\"></div><div class=\"col-auto ms-auto\"><div class=\"btn btn-primary\">")
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("</div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func 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("<div class=\"block\"><div class=\"row row-cols-1 row-cols-md-4 g-4\">")
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("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func 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("<section class=\"row first-class-group g-4\"><h1 class=\"title\">")
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -462,7 +659,7 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, subcategory := range container.Subcategories { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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) { 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) templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer { if !templ_7745c5c3_IsBuffer {
@ -487,17 +684,17 @@ func listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer)
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var20 := templ.GetChildren(ctx) templ_7745c5c3_Var29 := templ.GetChildren(ctx)
if templ_7745c5c3_Var20 == nil { if templ_7745c5c3_Var29 == nil {
templ_7745c5c3_Var20 = templ.NopComponent templ_7745c5c3_Var29 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block second-class-group\"><h2 class=\"title\"><a href=\"") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block second-class-group\"><h3 class=\"title\"><a href=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var21 templ.SafeURL = templ.SafeURL("/courses/" + categoryID + "/" + subcategory.ID) var templ_7745c5c3_Var30 templ.SafeURL = templ.SafeURL("/courses/" + categoryID + "/" + subcategory.ID)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var21))) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var30)))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -505,30 +702,30 @@ func listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var22 string var templ_7745c5c3_Var31 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name) templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 128, Col: 22} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 186, Col: 22}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-up-right-square\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm5.854 8.803a.5.5 0 1 1-.708-.707L9.243 6H6.475a.5.5 0 1 1 0-1h3.975a.5.5 0 0 1 .5.5v3.975a.5.5 0 1 1-1 0V6.707z\"></path></svg></a></h2><p class=\"visually-hidden\">") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-up-right-square\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm5.854 8.803a.5.5 0 1 1-.708-.707L9.243 6H6.475a.5.5 0 1 1 0-1h3.975a.5.5 0 0 1 .5.5v3.975a.5.5 0 1 1-1 0V6.707z\"></path></svg></a></h3><p class=\"visually-hidden\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var23 := `В категогрии ` templ_7745c5c3_Var32 := `В категогрии `
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var33 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name) templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -536,17 +733,17 @@ func listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var25 := `собраны ` templ_7745c5c3_Var34 := `собраны `
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var35 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(subcategory.Count)) templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(subcategory.Count))
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -554,8 +751,8 @@ func listCoursesThematicRow(categoryID string, subcategory SubcategoryContainer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var27 := `курсов. Раз в неделю мы обновляем информацию о всех курсах.` templ_7745c5c3_Var36 := `курсов. Раз в неделю мы обновляем информацию о всех курсах.`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -609,17 +806,25 @@ func listCoursesCard(info CourseInfo) templ.Component {
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var28 := templ.GetChildren(ctx) templ_7745c5c3_Var37 := templ.GetChildren(ctx)
if templ_7745c5c3_Var28 == nil { if templ_7745c5c3_Var37 == nil {
templ_7745c5c3_Var28 = templ.NopComponent templ_7745c5c3_Var37 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col\"><div class=\"card h-100\">") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col\" id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var29 = []any{"card-img-top"} _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(info.ID))
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...) if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><div class=\"card h-100\">")
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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -635,7 +840,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -643,8 +848,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var30 = []any{"card-body", cardTextSize(), "row"} var templ_7745c5c3_Var39 = []any{"card-body", cardTextSize(), "row"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var30...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var39...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -652,7 +857,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -660,12 +865,12 @@ func listCoursesCard(info CourseInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var31 string var templ_7745c5c3_Var40 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name) templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -673,8 +878,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var32 templ.SafeURL = templ.URL(info.OriginLink) var templ_7745c5c3_Var41 templ.SafeURL = templ.URL(info.OriginLink)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var32))) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var41)))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -682,8 +887,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var33 := `Go!` templ_7745c5c3_Var42 := `Go!`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -691,12 +896,12 @@ func listCoursesCard(info CourseInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var34 string var templ_7745c5c3_Var43 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice)) templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice))
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -704,8 +909,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Var35 := `rub.` templ_7745c5c3_Var44 := `rub.`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var44)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -728,12 +933,12 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var36 := templ.GetChildren(ctx) templ_7745c5c3_Var45 := templ.GetChildren(ctx)
if templ_7745c5c3_Var36 == nil { if templ_7745c5c3_Var45 == nil {
templ_7745c5c3_Var36 = templ.NopComponent templ_7745c5c3_Var45 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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) templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer { if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer() templ_7745c5c3_Buffer = templ.GetBuffer()
@ -755,7 +960,7 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -772,7 +977,7 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com
} }
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -63,13 +63,13 @@ type Pagination struct {
} }
templ pagination(p Pagination) { templ pagination(p Pagination) {
if p.Page > 0 { if p.Page > 0 && p.TotalPages > 0 {
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
<li <li
class={ class={
"page-item", "page-item",
templ.KV("disabled", p.Page > 0 && p.Page == 1), templ.KV("disabled", p.Page == 1),
} }
> >
<a href={ templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page-1)) } class="page-link">Previous</a> <a href={ templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page-1)) } class="page-link">Previous</a>

View File

@ -216,14 +216,14 @@ func pagination(p Pagination) templ.Component {
templ_7745c5c3_Var11 = templ.NopComponent templ_7745c5c3_Var11 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
if p.Page > 0 { if p.Page > 0 && p.TotalPages > 0 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav aria-label=\"Page navigation\"><ul class=\"pagination justify-content-center\">") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav aria-label=\"Page navigation\"><ul class=\"pagination justify-content-center\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 = []any{ var templ_7745c5c3_Var12 = []any{
"page-item", "page-item",
templ.KV("disabled", p.Page > 0 && p.Page == 1), templ.KV("disabled", p.Page == 1),
} }
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {

View File

@ -45,6 +45,29 @@ type stats struct {
CategoriesCount string CategoriesCount string
} }
type NameIDPair struct {
ID string
Name string
}
type SortingItem struct {
NameIDPair
Ascending bool
}
type SortingView struct {
Items []SortingItem
}
type CoursesFilterViewParams struct {
SelectedSchoolID string
Schools []NameIDPair
OrderBy string
OrderFields []NameIDPair
Ascending bool
}
type Category struct { type Category struct {
ID string ID string
Name string Name string
@ -62,8 +85,10 @@ type BreadcrumbsParams struct {
type FilterFormParams struct { type FilterFormParams struct {
BreadcrumbsParams BreadcrumbsParams
Schools CoursesFilterViewParams
AvailableLearningTypes []Category AvailableLearningTypes []Category
AvailableCourseThematics []Category AvailableCourseThematics []Category
Render bool
} }
type CourseInfo struct { type CourseInfo struct {
@ -98,6 +123,7 @@ type ListCoursesParams struct {
Categories []CategoryContainer Categories []CategoryContainer
Pagination Pagination Pagination Pagination
Items int Items int
Courses []CourseInfo
} }
func GetOrFallback[T comparable](value T, fallback T) T { func GetOrFallback[T comparable](value T, fallback T) T {

View File

@ -2,9 +2,11 @@ package http
import ( import (
"encoding/json" "encoding/json"
"fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"slices" "slices"
"sync"
"git.loyso.art/frx/kurious/internal/common/xslices" "git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/app/query" "git.loyso.art/frx/kurious/internal/kurious/app/query"
@ -33,7 +35,9 @@ func makeTemplListCoursesParams(counts map[string]domain.LearningTypeStat, in ..
coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in)) coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in))
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in)) subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
categoryByID := make(map[string]bootstrap.CategoryBaseInfo, len(in)) categoryByID := make(map[string]bootstrap.CategoryBaseInfo, len(in))
seenCourses := make(map[string]struct{}, len(in))
var out bootstrap.ListCoursesParams
xslices.ForEach(in, func(c domain.Course) { xslices.ForEach(in, func(c domain.Course) {
courseInfo := bootstrap.CourseInfo{ courseInfo := bootstrap.CourseInfo{
ID: c.ID, ID: c.ID,
@ -63,9 +67,15 @@ func makeTemplListCoursesParams(counts map[string]domain.LearningTypeStat, in ..
Count: counts[c.LearningTypeID].CourseThematic[c.ThematicID], Count: counts[c.LearningTypeID].CourseThematic[c.ThematicID],
} }
} }
if _, ok := seenCourses[c.ExternalID.Value()]; ok && c.ExternalID.Valid() {
return
}
out.Courses = append(out.Courses, courseInfo)
seenCourses[c.ExternalID.Value()] = struct{}{}
}) })
var out bootstrap.ListCoursesParams
for categoryID, subcategoriesID := range subcategoriesByCategories { for categoryID, subcategoriesID := range subcategoriesByCategories {
outCategory := bootstrap.CategoryContainer{ outCategory := bootstrap.CategoryContainer{
CategoryBaseInfo: categoryByID[categoryID], CategoryBaseInfo: categoryByID[categoryID],
@ -112,6 +122,9 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{ listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
CourseThematic: pathParams.CourseThematic, CourseThematic: pathParams.CourseThematic,
LearningType: pathParams.LearningType, LearningType: pathParams.LearningType,
OrganizationID: pathParams.School,
OrderBy: orderByListing.getField(pathParams.OrderBy),
Ascending: pathParams.Ascending,
Limit: pathParams.PerPage, Limit: pathParams.PerPage,
NextPageToken: pathParams.NextPageToken, NextPageToken: pathParams.NextPageToken,
Offset: offset, Offset: offset,
@ -124,6 +137,7 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
if handleError(ctx, err, w, c.log, "unable to load stats") { if handleError(ctx, err, w, c.log, "unable to load stats") {
return return
} }
params := makeTemplListCoursesParams(statsresult.StatsByLearningType, listCoursesResult.Courses...) params := makeTemplListCoursesParams(statsresult.StatsByLearningType, listCoursesResult.Courses...)
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{}) learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
@ -164,15 +178,50 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
}) })
} }
organizaions, err := c.app.Queries.ListOrganizationsStats.Handle(ctx, query.ListOrganizationsStats{})
if handleError(ctx, err, w, c.log, "unable to list organizations") {
return
}
slices.SortFunc(organizaions, func(a, b domain.OrganizationStat) int {
if a.CoursesCount > b.CoursesCount {
return -1
} else if a.CoursesCount < b.CoursesCount {
return 1
}
if a.ID > b.ID {
return 1
}
return -1
})
schools := xslices.Map(organizaions, func(in domain.OrganizationStat) bootstrap.NameIDPair {
return bootstrap.NameIDPair{
ID: in.ID,
Name: fmt.Sprintf("%s (count: %d)", in.Name, in.CoursesCount),
}
})
params = bootstrap.ListCoursesParams{ params = bootstrap.ListCoursesParams{
FilterForm: bootstrap.FilterFormParams{ FilterForm: bootstrap.FilterFormParams{
Render: true,
BreadcrumbsParams: bootstrap.BreadcrumbsParams{ BreadcrumbsParams: bootstrap.BreadcrumbsParams{
ActiveLearningType: params.FilterForm.ActiveLearningType, ActiveLearningType: params.FilterForm.ActiveLearningType,
ActiveCourseThematic: params.FilterForm.ActiveCourseThematic, ActiveCourseThematic: params.FilterForm.ActiveCourseThematic,
}, },
AvailableLearningTypes: params.FilterForm.AvailableLearningTypes, AvailableLearningTypes: params.FilterForm.AvailableLearningTypes,
AvailableCourseThematics: params.FilterForm.AvailableCourseThematics, AvailableCourseThematics: params.FilterForm.AvailableCourseThematics,
Schools: bootstrap.CoursesFilterViewParams{
SelectedSchoolID: pathParams.School,
Schools: schools,
Ascending: pathParams.Ascending,
OrderBy: pathParams.OrderBy,
OrderFields: orderByListing.asNameIDPair(),
}, },
},
Courses: params.Courses,
Categories: params.Categories, Categories: params.Categories,
Pagination: bootstrap.Pagination{ Pagination: bootstrap.Pagination{
Page: pathParams.Page, Page: pathParams.Page,
@ -279,3 +328,65 @@ func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
} }
span.SetStatus(codes.Ok, "request completed") span.SetStatus(codes.Ok, "request completed")
} }
var orderByListing = newOrderableContainer(
newOrderableUnit("pr", "Price", "full_price"),
newOrderableUnit("na", "Name", "name"),
newOrderableUnit("di", "Discount", "discount"),
newOrderableUnit("du", "Duration", "duration"),
newOrderableUnit("st", "Starts At", "starts_at"),
)
type orderableUnit struct {
ID string
Name string
Field string
}
func newOrderableUnit(id, name, field string) orderableUnit {
return orderableUnit{
ID: id,
Name: name,
Field: field,
}
}
type orderableContainer struct {
nameByID map[string]string
fieldByID map[string]string
cachedNameIDPair []bootstrap.NameIDPair
makeCache sync.Once
}
func (c *orderableContainer) asNameIDPair() []bootstrap.NameIDPair {
c.makeCache.Do(func() {
c.cachedNameIDPair = make([]bootstrap.NameIDPair, 0, len(c.nameByID))
for id, name := range c.nameByID {
c.cachedNameIDPair = append(c.cachedNameIDPair, bootstrap.NameIDPair{
ID: id,
Name: name,
})
}
})
return c.cachedNameIDPair
}
func (c *orderableContainer) getField(id string) string {
return c.fieldByID[id]
}
func newOrderableContainer(units ...orderableUnit) *orderableContainer {
nameByID := make(map[string]string, len(units))
fieldByID := make(map[string]string, len(units))
xslices.ForEach(units, func(u orderableUnit) {
nameByID[u.ID] = u.Name
fieldByID[u.ID] = u.Field
})
return &orderableContainer{
nameByID: nameByID,
fieldByID: fieldByID,
}
}

View File

@ -10,6 +10,7 @@ import (
"git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/common/xcontext" "git.loyso.art/frx/kurious/internal/common/xcontext"
"git.loyso.art/frx/kurious/internal/kurious/service" "git.loyso.art/frx/kurious/internal/kurious/service"
"git.loyso.art/frx/kurious/pkg/xdefault"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
@ -84,7 +85,7 @@ func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
return out, errors.NewValidationError("per_page", "bad per_page value") return out, errors.NewValidationError("per_page", "bad per_page value")
} }
} else { } else {
out.PerPage = 50 out.PerPage = 20
} }
if query.Has("page") { if query.Has("page") {
out.Page, err = strconv.Atoi(query.Get("page")) out.Page, err = strconv.Atoi(query.Get("page"))
@ -113,6 +114,13 @@ func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error)
out.LearningType = vars[LearningTypePathParam] out.LearningType = vars[LearningTypePathParam]
out.CourseThematic = vars[ThematicTypePathParam] out.CourseThematic = vars[ThematicTypePathParam]
out.School = r.URL.Query().Get("school_id")
out.OrderBy = xdefault.WithFallback(r.URL.Query().Get("order_by"), "price")
if r.URL.Query().Has("asc") {
out.Ascending, _ = strconv.ParseBool(r.URL.Query().Get("asc"))
}
return out, nil return out, nil
} }
@ -121,6 +129,9 @@ type listCoursesParams struct {
CourseThematic string CourseThematic string
LearningType string LearningType string
School string
OrderBy string
Ascending bool
} }
type IDNamePair struct { type IDNamePair struct {

View File

@ -89,6 +89,7 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.Co
GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log), GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log),
ListOrganzations: query.NewListOrganizationsHandler(organizationrepo, log), ListOrganzations: query.NewListOrganizationsHandler(organizationrepo, log),
ListOrganizationsStats: query.NewListOrganizationsStatsHandler(organizationrepo, log),
GetOrganization: query.NewGetOrganizationHandler(organizationrepo, log), GetOrganization: query.NewGetOrganizationHandler(organizationrepo, log),
}, },
} }