add opentelemetry tracing

This commit is contained in:
Aleksandr Trushkin
2024-04-02 15:23:22 +03:00
parent e7c2832865
commit 68810d93a7
40 changed files with 1459 additions and 3048 deletions

View File

@ -1 +1 @@
3c1808b7a88ab24b1cacf9a132073105
3cf236a901d03e42352790df844d58c5

View File

@ -43,14 +43,12 @@ tasks:
- "$GOBIN/golangci-lint run ./..."
deps:
- generate
- mocks
test:
run: once
cmds:
- go test ./internal/...
deps:
- generate
- mocks
build_web:
cmds:
- go build -o $GOBIN/kuriousweb -v -ldflags '{{.LDFLAGS}}' cmd/kuriweb/*.go

View File

@ -1,5 +0,0 @@
package main
func main() {
println("oh well")
}

View File

@ -11,7 +11,10 @@ import (
type Config struct {
Log config.Log `json:"log"`
YDB config.YDB `json:"ydb"`
Sqlite config.Sqlite `json:"sqlite"`
HTTP config.HTTP `json:"http"`
Tracing config.Trace `json:"tracing"`
DBEngine string `json:"db_engine"`
}
func readFromFile(path string, defaultConfigF func() Config) (Config, error) {

View File

@ -11,8 +11,12 @@ import (
"git.loyso.art/frx/kurious/internal/common/generator"
"git.loyso.art/frx/kurious/internal/common/xcontext"
xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http"
"git.loyso.art/frx/kurious/pkg/xdefault"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func makePathTemplate(params ...string) string {
@ -29,50 +33,32 @@ func makePathTemplate(params ...string) string {
return sb.String()
}
func setupHTTPWithTempl(srv xhttp.Server, router *mux.Router, log *slog.Logger) {
coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true)
func setupCoursesHTTP(srv xhttp.Server, router *mux.Router, _ *slog.Logger) {
coursesAPI := srv.Courses()
coursesAPI := srv.CoursesByTempl()
coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet)
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
coursesRouter.HandleFunc(coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
coursesRouter.HandleFunc(coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
}
func setupHTTPWithGoTemplates(srv xhttp.Server, router *mux.Router, log *slog.Logger) {
coursesAPI := srv.Courses(true)
router.Handle("/", http.RedirectHandler("/courses", http.StatusPermanentRedirect))
coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true)
coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet)
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
coursesRouter.HandleFunc(coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
coursesRouter.HandleFunc(coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
courseRouter := router.PathPrefix("/course").PathPrefix("/{course_id}").Subrouter()
courseRouter.HandleFunc("/", coursesAPI.Get).Methods(http.MethodGet)
courseRouter.HandleFunc("/short", coursesAPI.GetShort).Methods(http.MethodGet)
courseRouter.HandleFunc("/editdesc", coursesAPI.RenderEditDescription).Methods(http.MethodGet)
courseRouter.HandleFunc("/description", coursesAPI.UpdateCourseDescription).Methods(http.MethodPut)
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
muxHandleFunc(coursesRouter, "/", coursesAPI.Index).Methods(http.MethodGet)
muxHandleFunc(coursesRouter, coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
muxHandleFunc(coursesRouter, coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
}
func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server {
router := mux.NewRouter()
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
router.Use(
middlewareCustomWriterInjector(),
mux.CORSMethodMiddleware(router),
middlewareLogger(log),
middlewareTrace(),
)
router.Use(mux.CORSMethodMiddleware(router))
router.Use(middlewareLogger(log, cfg.Engine))
if cfg.Engine == "templ" {
setupHTTPWithTempl(srv, router, log)
} else {
setupHTTPWithGoTemplates(srv, router, log)
}
setupCoursesHTTP(srv, router, log)
if cfg.MountLive {
fs := http.FileServer(http.Dir("./assets/kurious/static/"))
@ -103,7 +89,7 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server
}
} else {
fs := kurious.AsHTTPFileHandler()
router.PathPrefix("/").Handler(fs).Methods(http.MethodGet)
router.PathPrefix("/*").Handler(fs).Methods(http.MethodGet)
}
return &http.Server{
@ -112,7 +98,46 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server
}
}
func middlewareLogger(log *slog.Logger, engine string) mux.MiddlewareFunc {
func middlewareCustomWriterInjector() mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wr := wrapWithCustomWriter(w)
next.ServeHTTP(wr, r)
})
}
}
func middlewareTrace() mux.MiddlewareFunc {
reqidAttr := attribute.Key("http.request_id")
statusAttr := attribute.Key("http.status_code")
payloadAttr := attribute.Key("http.payload_size")
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqid := xcontext.GetRequestID(ctx)
var span trace.Span
ctx, span = webtracer.Start(ctx, r.URL.String(), trace.WithAttributes(reqidAttr.String(reqid)))
defer span.End()
next.ServeHTTP(w, r.WithContext(ctx))
if wr, ok := w.(*customResponseWriter); ok {
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
span.SetAttributes(
statusAttr.Int(statusCode),
payloadAttr.Int(wr.wroteBytes),
)
if statusCode > 399 {
span.SetStatus(codes.Error, "error during request")
}
}
})
}
}
func middlewareLogger(log *slog.Logger) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -120,11 +145,12 @@ func middlewareLogger(log *slog.Logger, engine string) mux.MiddlewareFunc {
if requestID == "" {
requestID = generator.RandomInt64ID()
}
ctx = xcontext.WithLogFields(
ctx,
slog.String("request_id", requestID),
slog.String("engine", engine),
)
ctx = xcontext.WithRequestID(ctx, requestID)
xcontext.LogInfo(
ctx, log, "incoming request",
@ -134,12 +160,45 @@ func middlewareLogger(log *slog.Logger, engine string) mux.MiddlewareFunc {
start := time.Now()
next.ServeHTTP(w, r.WithContext(ctx))
elapsed := time.Since(start).Truncate(time.Millisecond)
elapsed := slog.Duration("elapsed", time.Since(start).Truncate(time.Millisecond))
xcontext.LogInfo(
ctx, log, "request processed",
slog.Duration("elapsed", elapsed),
logfields := make([]slog.Attr, 0, 3)
logfields = append(logfields, elapsed)
if wr, ok := w.(*customResponseWriter); ok {
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
logfields = append(
logfields,
slog.Int("status_code", statusCode),
slog.Int("bytes_wrote", wr.wroteBytes),
)
}
xcontext.LogInfo(ctx, log, "request processed", logfields...)
})
}
}
func wrapWithCustomWriter(origin http.ResponseWriter) *customResponseWriter {
return &customResponseWriter{
ResponseWriter: origin,
}
}
type customResponseWriter struct {
http.ResponseWriter
statusCode int
wroteBytes int
}
func (w *customResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *customResponseWriter) Write(data []byte) (n int, err error) {
n, err = w.ResponseWriter.Write(data)
w.wroteBytes += n
return n, err
}

View File

@ -46,6 +46,17 @@ func app(ctx context.Context) error {
log := config.NewSLogger(cfg.Log)
shutdownOtel, err := setupOtelSDK(ctx, cfg.Tracing)
if err != nil {
return fmt.Errorf("setting up otel sdk: %w", err)
}
defer func() {
err := shutdownOtel(ctx)
if err != nil {
xcontext.LogWithError(ctx, log, err, "shutting down sdk")
}
}()
sravniClient, err := sravni.NewClient(ctx, log, false)
if err != nil {
return fmt.Errorf("unable to make new sravni client: %w", err)
@ -71,9 +82,20 @@ func app(ctx context.Context) error {
mapper := adapters.NewMemoryMapper(courseThematcisMapped, learningTypeMapped)
var dbengine service.RepositoryEngine
switch cfg.DBEngine {
case "ydb":
dbengine = service.RepositoryEngineYDB
case "sqlite":
dbengine = service.RepositoryEngineSqlite
default:
dbengine = service.RepositoryEngineUnknown
}
app, err := service.NewApplication(ctx, service.ApplicationConfig{
LogConfig: cfg.Log,
YDB: cfg.YDB,
Sqlite: cfg.Sqlite,
Engine: dbengine,
}, mapper)
if err != nil {
return fmt.Errorf("making new application: %w", err)
@ -113,6 +135,11 @@ func app(ctx context.Context) error {
xcontext.LogInfo(ctx, log, "server closed successfuly")
err = shutdownOtel(sdctx)
if err != nil {
return fmt.Errorf("shutting down sdk: %w", err)
}
return nil
})

132
cmd/kuriweb/trace.go Normal file
View File

@ -0,0 +1,132 @@
package main
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"git.loyso.art/frx/kurious/internal/common/config"
"git.loyso.art/frx/kurious/pkg/xdefault"
"github.com/gorilla/mux"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
)
var webtracer = otel.Tracer("kuriweb")
type shutdownFunc func(context.Context) error
func setupOtelSDK(ctx context.Context, cfg config.Trace) (shutdown shutdownFunc, err error) {
var shutdownFuncs []shutdownFunc
shutdown = func(ctx context.Context) error {
var err error
for _, f := range shutdownFuncs {
err = errors.Join(err, f(ctx))
}
shutdownFuncs = nil
return err
}
handleError := func(inErr error) error {
err = errors.Join(inErr, shutdown(ctx))
return err
}
prop := newPropagator()
otel.SetTextMapPropagator(prop)
tracerProvider, err := newTraceProvider(ctx, cfg.Endpoint, cfg.LicenseKey)
if err != nil {
return nil, handleError(err)
}
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
meterProvider, err := newMeterProvider()
if err != nil {
return nil, handleError(err)
}
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
otel.SetMeterProvider(meterProvider)
return shutdown, nil
}
func newPropagator() propagation.TextMapPropagator {
return propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
}
const defaultNewRelicEndpoint = "otlp.eu01.nr-data.net:443"
func newTraceProvider(ctx context.Context, endpoint, licensekey string) (traceProvider *trace.TracerProvider, err error) {
opts := make([]trace.TracerProviderOption, 0, 2)
opts = append(
opts,
trace.WithSampler(trace.AlwaysSample()),
trace.WithResource(resource.Default()),
)
if licensekey != "" {
endpoint = xdefault.WithFallback(endpoint, defaultNewRelicEndpoint)
client, err := otlptracegrpc.New(
ctx,
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithHeaders(map[string]string{
"api-key": licensekey,
}),
)
if err != nil {
return nil, fmt.Errorf("making grpc client: %w", err)
}
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)),
)
}
traceProvider = trace.NewTracerProvider(opts...)
return traceProvider, nil
}
func newMeterProvider() (*metric.MeterProvider, error) {
metricExporter, err := stdoutmetric.New()
if err != nil {
return nil, err
}
meterProvider := metric.NewMeterProvider(
metric.WithReader(metric.NewPeriodicReader(metricExporter,
// Default is 1m. Set to 3s for demonstrative purposes.
metric.WithInterval(60*time.Second))),
)
return meterProvider, nil
}
func muxHandleFunc(router *mux.Router, path string, hf http.HandlerFunc) *mux.Route {
h := otelhttp.WithRouteTag(path, hf)
return router.Handle(path, h)
}

35
go.mod
View File

@ -12,18 +12,31 @@ require (
github.com/teris-io/cli v1.0.1
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2
github.com/ydb-platform/ydb-go-yc v0.12.1
golang.org/x/net v0.18.0
golang.org/x/sync v0.5.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0
go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc 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/sdk v1.24.0
go.opentelemetry.io/otel/sdk/metric v1.24.0
go.opentelemetry.io/otel/trace v1.24.0
golang.org/x/net v0.22.0
golang.org/x/sync v0.6.0
golang.org/x/time v0.5.0
modernc.org/sqlite v1.29.3
)
require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@ -34,13 +47,15 @@ require (
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 // indirect
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd // indirect
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect
golang.org/x/sys v0.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.41.0 // indirect

80
go.sum
View File

@ -527,6 +527,8 @@ github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0I
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
@ -573,6 +575,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -586,6 +590,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
@ -631,8 +640,9 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -680,8 +690,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S3
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
@ -704,6 +714,8 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@ -728,8 +740,9 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -774,8 +787,9 @@ github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@ -800,8 +814,6 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/teris-io/cli v1.0.1 h1:J6jnVHC552uqx7zT+Ux0++tIvLmJQULqxVhCid2u/Gk=
github.com/teris-io/cli v1.0.1/go.mod h1:V9nVD5aZ873RU/tQXLSXO8FieVPQhQvuNohsdsKXsGw=
github.com/vektra/mockery/v2 v2.42.1 h1:z7l3O4jCzRZat3rm9jpHc8lzpR8bs1VBii7bYtl3KQs=
github.com/vektra/mockery/v2 v2.42.1/go.mod h1:XNTE9RIu3deGAGQRVjP1VZxGpQNm0YedZx4oDs3prr8=
github.com/yandex-cloud/go-genproto v0.0.0-20211115083454-9ca41db5ed9e/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 h1:xCzizLC090MiLWEV3aziL5YIKrSVTRXX2DXlRGeQ6sA=
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
@ -834,9 +846,33 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/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/stdout/stdoutmetric v1.24.0 h1:JYE2HM7pZbOt5Jhk8ndWZTUWYOVift2cHjXVMkPdmdc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0/go.mod h1:yMb/8c6hVsnma0RpsBMNo0fEiQKeclawtgaIaOp2MLY=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8=
go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -964,8 +1000,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1010,8 +1046,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1090,8 +1126,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@ -1394,12 +1430,10 @@ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg=
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38=
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -1440,8 +1474,8 @@ google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsA
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@ -1459,8 +1493,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@ -1,121 +1,82 @@
<!doctype html>
<html lang="en">
<head>
<head>
<title>Test page</title>
<meta charset="UTF-8" />
<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"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" 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"
crossorigin="anonymous"
></script>
</head>
<body data-bs-theme="dark">
crossorigin="anonymous"></script>
</head>
<body data-bs-theme="dark">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Kurious</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup"
aria-expanded="false"
aria-label="Toggle navigation"
>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a
class="nav-link"
aria-current="page"
href="/index.html"
>Home</a
>
<a class="nav-link" aria-current="page" href="/index.html">Home</a>
<a class="nav-link" href="/courses.html">Courses</a>
<a class="nav-link active" href="/core.html"
>About us</a
>
<a class="nav-link active" href="/core.html">About us</a>
</div>
</div>
</div>
</nav>
<div class="container">
<nav
style="--bs-breadcrumb-divider: &quot;>&quot;"
aria-label="breadcrumb"
>
<nav style="--bs-breadcrumb-divider: '>'" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item" aria-current="page">
<a href="#">Course</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
Theme
</li>
<li class="breadcrumb-item active" aria-current="page">Theme</li>
</ol>
</nav>
<div class="row row-cols-1 row-cols-md-4 g-4">
<div class="col">
<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">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur
adipisicing elit.
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur
adipisicing elit. Enim omnis vero, reiciendis
obcaecati perferendis excepturi nostrum nobis
itaque modi dignissimos ...
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<!-- <a href="#" class="btn btn-primary">Go somewhere</a> -->
</div>
<div class="list-group">
<a href="#" class="btn btn-primary"
>Buy for 399.99$</a
>
<a href="#" class="btn btn-primary">Buy for 399.99$</a>
<small class="text-body-secondary"></small>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary col"
>399.99$</small
>
<small class="text-body-secondary col">399.99$</small>
</div>
</div>
</div>
<div class="col">
<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">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur
adipisicing elit.
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur
adipisicing elit. Enim omnis vero, reiciendis
obcaecati perferendis excepturi nostrum nobis
itaque modi dignissimos ...
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
@ -127,21 +88,15 @@
<div class="col">
<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">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur
adipisicing elit.
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur
adipisicing elit. Enim omnis vero, reiciendis
obcaecati perferendis excepturi nostrum nobis
itaque modi dignissimos ...
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
@ -153,21 +108,15 @@
<div class="col">
<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">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur
adipisicing elit.
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur
adipisicing elit. Enim omnis vero, reiciendis
obcaecati perferendis excepturi nostrum nobis
itaque modi dignissimos ...
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
@ -179,21 +128,15 @@
<div class="col">
<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">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur
adipisicing elit.
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur
adipisicing elit. Enim omnis vero, reiciendis
obcaecati perferendis excepturi nostrum nobis
itaque modi dignissimos ...
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
@ -204,5 +147,6 @@
</div>
</div>
</div>
</body>
</body>
</html>

View File

@ -1,64 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<head>
<title>Test page</title>
<meta charset="UTF-8" />
<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"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" 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"
crossorigin="anonymous"
></script>
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/assets/style.css" />
</head>
</head>
<body data-bs-theme="dark" style="margin: 0">
<body data-bs-theme="dark" style="margin: 0">
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
<div class="container-fluid">
<a class="navbar-brand" href="/index.html">Kurious</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<button 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>
</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">
<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 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 class="nav-item">
<a class="nav-link" href="/core.html"
>About us</a
>
<a class="nav-link" href="/core.html">About us</a>
</li>
</ul>
</div>
@ -68,19 +41,13 @@
<div class="container">
<section class="row header">
<nav
class="mt-4"
style="--bs-breadcrumb-divider: &quot;/&quot;"
aria-label="breadcrumb"
>
<nav class="mt-4" style="--bs-breadcrumb-divider: '/'" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#">Main</a></li>
<li class="breadcrumb-item" aria-current="page">
<a href="#">Languages</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
Japanese
</li>
<li class="breadcrumb-item active" aria-current="page">Japanese</li>
</ol>
</nav>
</section>
@ -90,22 +57,14 @@
<div class="input-group">
<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 value="1">Programming</option>
<option value="2">Design</option>
<option value="3">Business</option>
</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 value="1">Web development</option>
<option value="2">Backend</option>
@ -120,18 +79,11 @@
<section class="row first-class-group">
<h1 class="title">Languages</h1>
<p>
A languages category provides all courses to help learn
language
</p>
<p>A languages category provides all courses to help learn language</p>
<div class="filter-content d-flex mb-3">
<div class="p-2">
<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>Pick a school</option>
<option value="1">First school in the row</option>
<option value="2">
@ -142,11 +94,7 @@
</div>
<div class="p-2">
<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>Sort by</option>
<option value="1">One</option>
<option value="2">Two</option>
@ -167,25 +115,12 @@
<div class="row g-4">
<div class="col-12 col-md-6 col-lg-3">
<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">
<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">
<a
href="#"
class="btn text btn-outline-primary flex-grow-1"
>Open ></a
>
<span
class="input-group-text justify-content-end flex-fill"
>500$</span
>
<a href="#" class="btn text btn-outline-primary flex-grow-1">Open ></a>
<span class="input-group-text justify-content-end flex-fill">500$</span>
</div>
</div>
</div>
@ -202,5 +137,6 @@
</div>
</footer>
</div>
</body>
</body>
</html>

View File

@ -1,64 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<head>
<title>Test page</title>
<meta charset="UTF-8" />
<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"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" 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"
crossorigin="anonymous"
></script>
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/assets/style.css" />
</head>
</head>
<body data-bs-theme="dark" style="margin: 0">
<body data-bs-theme="dark" style="margin: 0">
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
<div class="container-fluid">
<a class="navbar-brand" href="/index.html">Kurious</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<button 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>
</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">
<li class="nav-item">
<a
class="nav-link active"
aria-current="page"
href="/index.html"
>Home</a
>
<a class="nav-link active" aria-current="page" href="/index.html">Home</a>
</li>
<li class="nav-item">
<a
class="nav-link"
aria-current="page"
href="/courses.html"
>Courses</a
>
<a class="nav-link" aria-current="page" href="/courses.html">Courses</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/core.html"
>About us</a
>
<a class="nav-link" href="/core.html">About us</a>
</li>
</ul>
</div>
@ -68,7 +41,90 @@
<div class="container">
<div class="row upper mb-4 text-center" style="min-height: 4rem">
<p class="justify-content-center">Some header about courses</p>
<p class="justify-content-center">Here you can find course for any taste</p>
</div>
<div class="container w-75">
<div class="row g-4">
<div class="col-12 col-md-8 col-lg-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Programming</h5>
<hr/>
<p>In this category you can find courses of types such as</p>
<p>web-development, backend development, frontend developent</p>
<p>This category contains <span>128</span> courses.</p>
<div class="d-flex justify-content-between align-items-center">
<a href="#" class="btn btn-sm btn-outline-primary col-6">
Open
</a>
<small class="text-body-secondary">128 items</small>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Programming</h5>
<hr/>
<p>In this category you can find courses of types such as</p>
<p>web-development, backend development, frontend developent</p>
<p>This category contains <span>128</span> courses.</p>
<div class="d-flex justify-content-between align-items-center">
<a href="#" class="btn btn-sm btn-outline-primary col-6">
Open
</a>
<small class="text-body-secondary">128 items</small>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Programming</h5>
<hr/>
<p>In this category you can find courses of types such as</p>
<p>web-development, backend development, frontend developent</p>
<p>This category contains <span>128</span> courses.</p>
<div class="d-flex justify-content-between align-items-center">
<a href="#" class="btn btn-sm btn-outline-primary col-6">
Open
</a>
<small class="text-body-secondary">128 items</small>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Programming</h5>
<hr/>
<p>In this category you can find courses of types such as</p>
<p>web-development, backend development, frontend developent</p>
<p>This category contains <span>128</span> courses.</p>
<div class="d-flex justify-content-between align-items-center">
<a href="#" class="btn btn-sm btn-outline-primary col-6">
Open
</a>
<small class="text-body-secondary">128 items</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row categories">
@ -122,5 +178,6 @@
</div>
</div>
</div>
</body>
</body>
</html>

View File

@ -3,5 +3,4 @@ package config
type HTTP struct {
ListenAddr string `json:"listen_addr"`
MountLive bool `json:"mount_live"`
Engine string `json:"engine"`
}

View File

@ -0,0 +1,6 @@
package config
type Trace struct {
Endpoint string `json:"endpoint"`
LicenseKey string `json:"license_key"`
}

View File

@ -2,11 +2,24 @@ package decorator
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
"time"
"git.loyso.art/frx/kurious/internal/common/xcontext"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var (
commandAttribute = attribute.Key("command_name")
queryAttribute = attribute.Key("query_name")
argsAttribute = attribute.Key("args")
apiTracer = otel.Tracer("cq")
)
type commandLoggingDecorator[T any] struct {
@ -18,6 +31,17 @@ func (c commandLoggingDecorator[T]) Handle(ctx context.Context, cmd T) (err erro
handlerName := getTypeName[T]()
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName))
var argsBuilder strings.Builder
_ = json.NewEncoder(&argsBuilder).Encode(cmd)
var span trace.Span
ctx, span = apiTracer.Start(ctx, handlerName)
span.SetAttributes(
commandAttribute.String(handlerName),
argsAttribute.String(argsBuilder.String()),
)
xcontext.LogDebug(ctx, c.log, "executing command")
start := time.Now()
@ -27,7 +51,9 @@ func (c commandLoggingDecorator[T]) Handle(ctx context.Context, cmd T) (err erro
xcontext.LogInfo(ctx, c.log, "command executed successfuly", elapsed)
} else {
xcontext.LogError(ctx, c.log, "command execution failed", elapsed, slog.Any("err", err))
span.RecordError(err)
}
span.End()
}()
return c.base.Handle(ctx, cmd)
@ -41,6 +67,17 @@ type queryLoggingDecorator[Q, U any] struct {
func (q queryLoggingDecorator[Q, U]) Handle(ctx context.Context, query Q) (entity U, err error) {
handlerName := getTypeName[Q]()
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName))
var argsBuilder strings.Builder
_ = json.NewEncoder(&argsBuilder).Encode(query)
var span trace.Span
ctx, span = apiTracer.Start(ctx, handlerName)
span.SetAttributes(
queryAttribute.String(handlerName),
argsAttribute.String(argsBuilder.String()),
)
xcontext.LogDebug(ctx, q.log, "executing command")
start := time.Now()
@ -50,7 +87,10 @@ func (q queryLoggingDecorator[Q, U]) Handle(ctx context.Context, query Q) (entit
xcontext.LogInfo(ctx, q.log, "command executed successfuly", elapsed)
} else {
xcontext.LogError(ctx, q.log, "command execution failed", elapsed, slog.Any("err", err))
span.RecordError(err)
}
now := time.Now()
span.End(trace.WithTimestamp(now))
}()
return q.base.Handle(ctx, query)

View File

@ -6,6 +6,16 @@ import (
)
type ctxLogKey struct{}
type ctxRequestID struct{}
func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, ctxRequestID{}, requestID)
}
func GetRequestID(ctx context.Context) string {
reqid, _ := ctx.Value(ctxRequestID{}).(string)
return reqid
}
type ctxLogAttrStore struct {
attrs []slog.Attr

View File

@ -1,5 +1,10 @@
package xslices
import (
"crypto/rand"
"math/big"
)
func ForEach[T any](items []T, f func(T)) {
for _, item := range items {
f(item)
@ -13,3 +18,12 @@ func AsMap[T any, U comparable](items []T, f func(T) U) map[U]struct{} {
})
return out
}
func Shuffle[T any](items []T) {
maxnum := big.NewInt(int64(len(items)))
for i := range items {
swapWith, _ := rand.Int(rand.Reader, maxnum)
swapWithIdx := int(swapWith.Int64())
items[i], items[swapWithIdx] = items[swapWithIdx], items[i]
}
}

View File

@ -106,11 +106,11 @@ func buttonRedirect(id, title string, linkTo string) templ.Component {
func onclickRedirect(id, to string) templ.ComponentScript {
return templ.ComponentScript{
Name: `__templ_onclickRedirect_47ae`,
Function: `function __templ_onclickRedirect_47ae(id, to){document.getElementById(id).onclick = () => {
Name: `__templ_onclickRedirect_5c43`,
Function: `function __templ_onclickRedirect_5c43(id, to){document.getElementById(id).onclick = () => {
location.href = to
}}`,
Call: templ.SafeScript(`__templ_onclickRedirect_47ae`, id, to),
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_47ae`, id, to),
Call: templ.SafeScript(`__templ_onclickRedirect_5c43`, id, to),
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_5c43`, id, to),
}
}

View File

@ -25,10 +25,10 @@ script breadcrumbsLoad() {
}
templ breadcrumbsItem(text, link string, isActive bool) {
<li class={"breadcrumb-item", templ.KV("active", isActive)}>
<li class={ "breadcrumb-item", templ.KV("active", isActive) }>
if link != "" {
<a
href={templ.URL(link)}
href={ templ.URL(link) }
itemprop="url"
aria-label="breadcrumb"
>{ text }</a>
@ -39,9 +39,8 @@ templ breadcrumbsItem(text, link string, isActive bool) {
}
templ breadcrumNode(params BreadcrumbsParams) {
// TODO: add divider to nav style
<nav
class={"mt-4", breadcrumbSymbol()}
class={ "mt-4", breadcrumbSymbol() }
aria-label="breadcrumbs"
itemprop="breadcrumb"
itemtype="https://schema.org/BreadcrumbList"
@ -49,7 +48,6 @@ templ breadcrumNode(params BreadcrumbsParams) {
>
<ol class="breadcrumb">
@breadcrumbsItem("Курсы", "/courses", params.ActiveLearningType.Empty())
if !params.ActiveLearningType.Empty() {
@breadcrumbsItem(
params.ActiveLearningType.Name,
@ -57,7 +55,6 @@ templ breadcrumNode(params BreadcrumbsParams) {
params.ActiveCourseThematic.Empty(),
)
}
if !params.ActiveCourseThematic.Empty() {
@breadcrumbsItem(
params.ActiveCourseThematic.Name,
@ -84,30 +81,28 @@ templ listCoursesSectionFilters(params FilterFormParams) {
<div class="col-8">
<form id="filter-form" class="input-group">
<span class="input-group-text">Filter courses</span>
<select
id="learning-type-filter"
class={"form-select"}
class={ "form-select" }
>
<option value="" selected?={params.ActiveLearningType.ID==""}>All</option>
<option value="" selected?={ params.ActiveLearningType.ID=="" }>All</option>
for _, learningType := range params.AvailableLearningTypes {
<option
selected?={params.ActiveLearningType.ID==learningType.ID}
value={learningType.ID}
>{learningType.Name}</option>
selected?={ params.ActiveLearningType.ID==learningType.ID }
value={ learningType.ID }
>{ learningType.Name }</option>
}
</select>
<select
id="course-thematic-filter"
class={"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)}
class={ "form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0) }
>
<option value="" selected?={params.ActiveLearningType.ID==""}>All</option>
<option value="" selected?={ params.ActiveLearningType.ID=="" }>All</option>
for _, courseThematic := range params.AvailableCourseThematics {
<option
selected?={params.ActiveCourseThematic.ID==courseThematic.ID}
value={courseThematic.ID}
>{courseThematic.Name}</option>
selected?={ params.ActiveCourseThematic.ID==courseThematic.ID }
value={ courseThematic.ID }
>{ courseThematic.Name }</option>
}
</select>
<button id="filter-course-thematic" class="btn btn-outline-secondary" type="submit">Go</button>
@ -119,8 +114,7 @@ templ listCoursesSectionFilters(params FilterFormParams) {
templ listCoursesLearning(containers []CategoryContainer) {
for _, container := range containers {
<section class="row first-class-group">
<h1 class="title">{container.Name}</h1>
<h1 class="title">{ container.Name }</h1>
for _, subcategory := range container.Subcategories {
@listCoursesThematicRow(subcategory)
}
@ -128,12 +122,10 @@ templ listCoursesLearning(containers []CategoryContainer) {
}
}
templ listCoursesThematicRow(subcategory SubcategoryContainer) {
<div class="block second-class-group">
<h2 class="title">{subcategory.Name}</h2>
<p>В категогрии {subcategory.Name} собраны {strconv.Itoa(subcategory.Count)} курсов. Раз в неделю мы обновляем информацию о всех курсах.</p>
<h2 class="title">{ subcategory.Name }</h2>
<p>В категогрии { subcategory.Name } собраны { strconv.Itoa(subcategory.Count) } курсов. Раз в неделю мы обновляем информацию о всех курсах.</p>
<div class="row row-cols-1 row-cols-md-4 g-4">
for _, info := range subcategory.Courses {
@listCoursesCard(info)
@ -147,7 +139,6 @@ css myImg() {
min-width: 19rem;
}
css cardTextSize() {
min-height: 12rem;
}
@ -156,16 +147,16 @@ templ listCoursesCard(info CourseInfo) {
// <div class="col-12 col-md-6 col-lg-3">
<div class="col">
<div class="card h-100">
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128")} alt="Course picture" class={"card-img-top"}/>
<div class={"card-body", cardTextSize(), "row"}>
<h5 class="card-title">{info.Name}</h5>
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128") } alt="Course picture" class={ "card-img-top" }/>
<div class={ "card-body", cardTextSize(), "row" }>
<h5 class="card-title">{ info.Name }</h5>
<div class="input-group d-flex align-self-end">
<a
href={ templ.URL(info.OriginLink) }
class="btn text btn-outline-primary flex-grow-1"
>Go!</a>
<span class="input-group-text justify-content-end flex-fill">
{strconv.Itoa(info.FullPrice)} rub.
{ strconv.Itoa(info.FullPrice) } rub.
</span>
</div>
</div>

View File

@ -16,8 +16,8 @@ import "strconv"
func breadcrumbsLoad() templ.ComponentScript {
return templ.ComponentScript{
Name: `__templ_breadcrumbsLoad_9a1d`,
Function: `function __templ_breadcrumbsLoad_9a1d(){const formFilterOnSubmit = event => {
Name: `__templ_breadcrumbsLoad_e656`,
Function: `function __templ_breadcrumbsLoad_e656(){const formFilterOnSubmit = event => {
event.preventDefault();
const lt = document.getElementById('learning-type-filter');
@ -35,8 +35,8 @@ func breadcrumbsLoad() templ.ComponentScript {
if (ff === null) return;
ff.addEventListener('submit', formFilterOnSubmit);
});}`,
Call: templ.SafeScript(`__templ_breadcrumbsLoad_9a1d`),
CallInline: templ.SafeScriptInline(`__templ_breadcrumbsLoad_9a1d`),
Call: templ.SafeScript(`__templ_breadcrumbsLoad_e656`),
CallInline: templ.SafeScriptInline(`__templ_breadcrumbsLoad_e656`),
}
}
@ -322,7 +322,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(learningType.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 96, Col: 25}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 92, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@ -399,7 +399,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 109, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 104, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@ -451,7 +451,7 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component {
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(container.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 121, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 116, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@ -499,7 +499,7 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 133, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 126, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
@ -517,7 +517,7 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 134, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 127, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
@ -535,7 +535,7 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(subcategory.Count))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 134, Col: 95}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 127, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@ -654,7 +654,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 160, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 151, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
@ -685,7 +685,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 167, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 158, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil {

View File

@ -0,0 +1,58 @@
package bootstrap
import "strconv"
type IndexCourseCategoryItem struct {
ID string
Name string
Description string
Count int
}
// courseItemCard is a card that renders a single course thematic item
// that holds multiple learning types. It expected to have a basic description
// and an amount of items.
templ courseItemCard(item IndexCourseCategoryItem) {
<div class="card">
<div class="card-body">
<h5 class="card-title">{ item.Name }</h5>
<hr/>
<p>{ item.Description }</p>
<div class="d-flex justify-content-between align-items-center">
<a
href={ templ.URL("/courses/" + item.ID) }
class="btn btn-sm btn-outline-primary col-6"
>
Open
</a>
<small class="text-body-secondary">
{ strconv.Itoa(item.Count) } items.
</small>
</div>
</div>
</div>
}
templ courseCategory(items []IndexCourseCategoryItem) {
<div class="container w-75">
<div class="row g-4">
for _, item := range items {
<div class="col-12 col-md-8 col-lg-4">
@courseItemCard(item)
</div>
}
</div>
</div>
}
type MainPageParams struct{
Breadcrumbs BreadcrumbsParams
Categories []IndexCourseCategoryItem
}
templ MainPage(pageType PageKind, s stats, params MainPageParams) {
@root(pageType, s) {
@listCoursesSectionHeader(params.Breadcrumbs)
@courseCategory(params.Categories)
}
}

View File

@ -0,0 +1,207 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.513
package bootstrap
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "strconv"
type IndexCourseCategoryItem struct {
ID string
Name string
Description string
Count int
}
// courseItemCard is a card that renders a single course thematic item
// that holds multiple learning types. It expected to have a basic description
// and an amount of items.
func courseItemCard(item IndexCourseCategoryItem) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"card\"><div class=\"card-body\"><h5 class=\"card-title\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 17, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h5><hr><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 19, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"d-flex justify-content-between align-items-center\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = templ.URL("/courses/" + item.ID)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"btn btn-sm btn-outline-primary col-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var5 := `Open`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> <small class=\"text-body-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(item.Count))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 28, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var7 := `items.`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</small></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func courseCategory(items []IndexCourseCategoryItem) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container w-75\"><div class=\"row g-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range items {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-12 col-md-8 col-lg-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = courseItemCard(item).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
type MainPageParams struct {
Breadcrumbs BreadcrumbsParams
Categories []IndexCourseCategoryItem
}
func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var10 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
templ_7745c5c3_Err = listCoursesSectionHeader(params.Breadcrumbs).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = courseCategory(params.Categories).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -2,352 +2,267 @@ package http
import (
"encoding/json"
"html/template"
"fmt"
"log/slog"
"net/http"
"sort"
"strconv"
"slices"
"strings"
"git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/common/xcontext"
"git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/app/command"
"git.loyso.art/frx/kurious/internal/kurious/app/query"
"git.loyso.art/frx/kurious/internal/kurious/domain"
"git.loyso.art/frx/kurious/internal/kurious/ports/http/bootstrap"
"git.loyso.art/frx/kurious/internal/kurious/service"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type courseServer struct {
var (
paramsAttr = attribute.Key("params")
webtracer = otel.Tracer("http")
)
type courseTemplServer struct {
app service.Application
log *slog.Logger
useTailwind bool
}
type pagination struct {
nextPageToken string
perPage int
}
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
query := r.URL.Query()
out.nextPageToken = query.Get("next")
if query.Has("per_page") {
out.perPage, err = strconv.Atoi(query.Get("per_page"))
if err != nil {
return out, errors.NewValidationError("per_page", "bad per_page value")
}
} else {
out.perPage = 50
}
return out, nil
}
const (
LearningTypePathParam = "learning_type"
ThematicTypePathParam = "thematic_type"
)
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
out.pagination, err = parsePaginationFromQuery(r)
if err != nil {
return out, err
}
vars := mux.Vars(r)
out.learningType = vars[LearningTypePathParam]
out.courseThematic = vars[ThematicTypePathParam]
return out, nil
}
type listCoursesParams struct {
pagination
courseThematic string
learningType string
}
type baseInfo struct {
ID string
Name string
Description string
}
type categoryInfo struct {
baseInfo
Subcategories []subcategoryInfo
}
type subcategoryInfo struct {
baseInfo
Courses []domain.Course
}
type IDNamePair struct {
ID string
Name string
IsActive bool
}
type listCoursesTemplateParams struct {
Categories []categoryInfo
NextPageToken string
AvailableLearningTypes []IDNamePair
AvailableCourseThematics []IDNamePair
ActiveLearningType string
LearningTypeName string
ActiveCourseThematic string
CourseThematicName string
}
func mapDomainCourseToTemplate(in ...domain.Course) listCoursesTemplateParams {
coursesBySubcategory := make(map[string][]domain.Course, len(in))
func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) bootstrap.ListCoursesParams {
coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in))
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
categoryByID := make(map[string]baseInfo, len(in))
categoryByID := make(map[string]bootstrap.CategoryBaseInfo, len(in))
xslices.ForEach(in, func(c domain.Course) {
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], c)
courseInfo := bootstrap.CourseInfo{
ID: c.ID,
Name: c.Name,
FullPrice: int(c.FullPrice),
ImageLink: c.ImageLink,
OriginLink: c.OriginLink,
}
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], courseInfo)
if _, ok := subcategoriesByCategories[c.LearningTypeID]; !ok {
subcategoriesByCategories[c.LearningTypeID] = map[string]struct{}{}
}
subcategoriesByCategories[c.LearningTypeID][c.ThematicID] = struct{}{}
if _, ok := categoryByID[c.LearningTypeID]; !ok {
categoryByID[c.LearningTypeID] = baseInfo{
categoryByID[c.LearningTypeID] = bootstrap.CategoryBaseInfo{
ID: c.LearningTypeID,
Name: c.LearningType,
}
}
if _, ok := categoryByID[c.ThematicID]; !ok {
categoryByID[c.ThematicID] = baseInfo{
categoryByID[c.ThematicID] = bootstrap.CategoryBaseInfo{
ID: c.ThematicID,
Name: c.Thematic,
Count: counts[c.ThematicID],
}
}
})
var out listCoursesTemplateParams
for category, subcategoryMap := range subcategoriesByCategories {
outCategory := categoryInfo{
baseInfo: categoryByID[category],
var out bootstrap.ListCoursesParams
for categoryID, subcategoriesID := range subcategoriesByCategories {
outCategory := bootstrap.CategoryContainer{
CategoryBaseInfo: categoryByID[categoryID],
}
for subcategory := range subcategoryMap {
outSubCategory := subcategoryInfo{
baseInfo: categoryByID[subcategory],
Courses: coursesBySubcategory[subcategory],
for subcategoryID := range subcategoriesID {
outSubcategory := bootstrap.SubcategoryContainer{
CategoryBaseInfo: categoryByID[subcategoryID],
Courses: coursesBySubcategory[subcategoryID],
}
outCategory.Subcategories = append(outCategory.Subcategories, outSubCategory)
outCategory.Subcategories = append(outCategory.Subcategories, outSubcategory)
}
sort.Slice(outCategory.Subcategories, func(i, j int) bool {
return outCategory.Subcategories[i].ID < outCategory.Subcategories[j].ID
})
out.Categories = append(out.Categories, outCategory)
}
sort.Slice(out.Categories, func(i, j int) bool {
return out.Categories[i].ID < out.Categories[j].ID
})
return out
}
func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
params, err := parseListCoursesParams(r)
var span trace.Span
ctx, span = webtracer.Start(ctx, "list")
defer func() {
span.End()
}()
stats := bootstrap.MakeNewStats(10_240, 2_560_000, 1800)
pathParams, err := parseListCoursesParams(r)
if handleError(ctx, err, w, c.log, "unable to parse list courses params") {
return
}
result, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
CourseThematic: params.courseThematic,
LearningType: params.learningType,
Limit: params.perPage,
NextPageToken: params.nextPageToken,
jsonParams, _ := json.Marshal(pathParams)
span.SetAttributes(paramsAttr.String(string(jsonParams)))
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
CourseThematic: pathParams.CourseThematic,
LearningType: pathParams.LearningType,
Limit: pathParams.PerPage,
NextPageToken: pathParams.NextPageToken,
})
if handleError(ctx, err, w, c.log, "unable to list courses") {
return
}
courses := result.Courses
templateCourses := mapDomainCourseToTemplate(courses...)
templateCourses.NextPageToken = result.NextPageToken
params := makeTemplListCoursesParams(listCoursesResult.AvailableCoursesOfSub, listCoursesResult.Courses...)
learningTypeList, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
if handleError(ctx, err, w, c.log, "unable to list learning types") {
return
}
templateCourses.AvailableLearningTypes = xslices.Map(learningTypeList.LearningTypes, func(in query.LearningType) IDNamePair {
if in.ID == params.learningType {
templateCourses.LearningTypeName = in.Name
}
return IDNamePair{
params.FilterForm.AvailableLearningTypes = xslices.Map(learningTypeResult.LearningTypes, func(in query.LearningType) bootstrap.Category {
outcategory := bootstrap.Category{
ID: in.ID,
Name: in.Name,
IsActive: in.ID == params.learningType,
}
if in.ID == pathParams.LearningType {
params.FilterForm.ActiveLearningType = outcategory
}
return outcategory
})
templateCourses.ActiveLearningType = params.learningType
templateCourses.ActiveCourseThematic = params.courseThematic
if params.learningType != "" {
if pathParams.LearningType != "" {
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
LearningTypeID: params.learningType,
LearningTypeID: pathParams.LearningType,
})
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
return
}
templateCourses.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) IDNamePair {
if in.ID == params.courseThematic {
templateCourses.CourseThematicName = in.Name
}
return IDNamePair{
params.FilterForm.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) bootstrap.Category {
outcategory := bootstrap.Category{
ID: in.ID,
Name: in.Name,
IsActive: in.ID == params.courseThematic,
}
if pathParams.CourseThematic == in.ID {
params.FilterForm.BreadcrumbsParams.ActiveCourseThematic = outcategory
}
return outcategory
})
}
var tmpl *template.Template
if c.useTailwind {
tmpl = getTemplateHTMLBySpecificFiles(ctx, c.log, "list.html")
c.log.DebugContext(
ctx, "using bootstrap",
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
)
params = bootstrap.ListCoursesParams{
FilterForm: bootstrap.FilterFormParams{
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
ActiveLearningType: params.FilterForm.ActiveLearningType,
ActiveCourseThematic: params.FilterForm.ActiveCourseThematic,
},
AvailableLearningTypes: params.FilterForm.AvailableLearningTypes,
AvailableCourseThematics: params.FilterForm.AvailableCourseThematics,
},
Categories: params.Categories,
}
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.CategoryContainer) int {
if lhs.Count > rhs.Count {
return 1
} else if lhs.Count < rhs.Count {
return -1
} else {
tmpl = getCoreTemplate(ctx, c.log)
return 0
}
err = tmpl.ExecuteTemplate(w, "courses", templateCourses)
if handleError(ctx, err, w, c.log, "unable to execute template") {
})
span.AddEvent("starting to render")
err = bootstrap.ListCourses(bootstrap.PageCourses, stats, params).Render(ctx, w)
span.AddEvent("render finished")
if handleError(ctx, err, w, c.log, "unable to render list courses") {
return
}
}
func (c courseServer) Get(w http.ResponseWriter, r *http.Request) {
func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := mux.Vars(r)["course_id"]
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
ID: id,
var span trace.Span
ctx, span = webtracer.Start(ctx, "index")
defer func() {
span.End()
}()
stats := bootstrap.MakeNewStats(1, 2, 3)
coursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{})
if handleError(ctx, err, w, c.log, "unable to list courses") {
return
}
params := bootstrap.MainPageParams{
Categories: []bootstrap.IndexCourseCategoryItem{},
}
coursesByLearningType := make(map[IDNamePair][]domain.Course)
xslices.ForEach(coursesResult.Courses, func(in domain.Course) {
pair := IDNamePair{
ID: in.LearningTypeID,
Name: in.LearningType,
}
coursesByLearningType[pair] = append(coursesByLearningType[pair], in)
})
if handleError(ctx, err, w, c.log, "unable to get course") {
return
for learningTypeInfo, courses := range coursesByLearningType {
category := bootstrap.IndexCourseCategoryItem{
ID: learningTypeInfo.ID,
Name: learningTypeInfo.Name,
Count: len(courses),
}
payload, err := json.MarshalIndent(course, "", " ")
if handleError(ctx, err, w, c.log, "unable to marshal json") {
return
xslices.Shuffle(courses)
if len(courses) > 3 {
courses = courses[:3]
}
w.Header().Set("content-type", "application/json")
w.Header().Set("content-length", strconv.Itoa(len(payload)))
_, err = w.Write([]byte(payload))
if err != nil {
xcontext.LogWithWarnError(ctx, c.log, err, "unable to write a message")
}
}
func (c courseServer) GetShort(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := mux.Vars(r)["course_id"]
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
ID: id,
names := xslices.Map(courses, func(in domain.Course) string {
return in.Name
})
if handleError(ctx, err, w, c.log, "unable to get course") {
return
namesStr := strings.Join(names, ",")
category.Description = fmt.Sprintf(
"Here you can find courses"+
" such as %s",
namesStr,
)
params.Categories = append(params.Categories, category)
}
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "course_info", course)
if handleError(ctx, err, w, c.log, "unable to execute template") {
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.IndexCourseCategoryItem) int {
if lhs.Count < rhs.Count {
return 1
} else if lhs.Count > rhs.Count {
return -1
}
return 0
})
span.AddEvent("starting to render")
err = bootstrap.MainPage(bootstrap.PageIndex, stats, params).Render(ctx, w)
span.AddEvent("render finished")
if handleError(ctx, err, w, c.log, "rendeting template") {
return
}
}
func (c courseServer) RenderEditDescription(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := mux.Vars(r)["course_id"]
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
ID: id,
})
if handleError(ctx, err, w, c.log, "unable to get course") {
return
}
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "edit_description", course)
if handleError(ctx, err, w, c.log, "unable to execute template") {
return
}
}
func (c courseServer) UpdateCourseDescription(w http.ResponseWriter, r *http.Request) {
type requestModel struct {
ID string `json:"-"`
Text string `json:"description"`
}
ctx := r.Context()
var req requestModel
req.ID = mux.Vars(r)["course_id"]
err := json.NewDecoder(r.Body).Decode(&req)
if handleError(ctx, err, w, c.log, "unable to read body") {
return
}
err = c.app.Commands.UpdateCourseDescription.Handle(ctx, command.UpdateCourseDescription{
ID: req.ID,
Description: req.Text,
})
if handleError(ctx, err, w, c.log, "unable to update course description") {
return
}
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
ID: req.ID,
})
if handleError(ctx, err, w, c.log, "unable to get course") {
return
}
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "course_info", course)
if handleError(ctx, err, w, c.log, "unable to execute template") {
return
}
}
func (c courseServer) UdpateDescription(w http.ResponseWriter, r *http.Request) {
type requestModel struct {
ID string `json:"id"`
Text string `json:"text"`
}
ctx := r.Context()
var req requestModel
err := json.NewDecoder(r.Body).Decode(&req)
if handleError(ctx, err, w, c.log, "unable to read body") {
return
}
err = c.app.Commands.UpdateCourseDescription.Handle(ctx, command.UpdateCourseDescription{
ID: req.ID,
Description: req.Text,
})
if handleError(ctx, err, w, c.log, "unable to update course description") {
return
}
w.WriteHeader(http.StatusOK)
}

View File

@ -1,184 +0,0 @@
package http
import (
"log/slog"
"net/http"
"git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/app/query"
"git.loyso.art/frx/kurious/internal/kurious/domain"
"git.loyso.art/frx/kurious/internal/kurious/ports/http/bootstrap"
xtempl "git.loyso.art/frx/kurious/internal/kurious/ports/http/templ"
"git.loyso.art/frx/kurious/internal/kurious/service"
)
type courseTemplServer struct {
app service.Application
log *slog.Logger
}
func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) xtempl.ListCoursesParams {
coursesBySubcategory := make(map[string][]xtempl.CourseInfo, len(in))
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
categoryByID := make(map[string]xtempl.CategoryBaseInfo, len(in))
xslices.ForEach(in, func(c domain.Course) {
courseInfo := xtempl.CourseInfo{
ID: c.ID,
Name: c.Name,
FullPrice: int(c.FullPrice),
ImageLink: c.ImageLink,
OriginLink: c.OriginLink,
}
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], courseInfo)
if _, ok := subcategoriesByCategories[c.LearningTypeID]; !ok {
subcategoriesByCategories[c.LearningTypeID] = map[string]struct{}{}
}
subcategoriesByCategories[c.LearningTypeID][c.ThematicID] = struct{}{}
if _, ok := categoryByID[c.LearningTypeID]; !ok {
categoryByID[c.LearningTypeID] = xtempl.CategoryBaseInfo{
ID: c.LearningTypeID,
Name: c.LearningType,
}
}
if _, ok := categoryByID[c.ThematicID]; !ok {
categoryByID[c.ThematicID] = xtempl.CategoryBaseInfo{
ID: c.ThematicID,
Name: c.Thematic,
Count: counts[c.ThematicID],
}
}
})
var out xtempl.ListCoursesParams
for categoryID, subcategoriesID := range subcategoriesByCategories {
outCategory := xtempl.CategoryContainer{
CategoryBaseInfo: categoryByID[categoryID],
}
for subcategoryID := range subcategoriesID {
outSubcategory := xtempl.SubcategoryContainer{
CategoryBaseInfo: categoryByID[subcategoryID],
Courses: coursesBySubcategory[subcategoryID],
}
outCategory.Subcategories = append(outCategory.Subcategories, outSubcategory)
}
out.Categories = append(out.Categories, outCategory)
}
return out
}
const useBootstrap = true
func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
stats := xtempl.MakeNewStats(10_240, 2_560_000, 1800)
pathParams, err := parseListCoursesParams(r)
if handleError(ctx, err, w, c.log, "unable to parse list courses params") {
return
}
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
CourseThematic: pathParams.courseThematic,
LearningType: pathParams.learningType,
Limit: pathParams.perPage,
NextPageToken: pathParams.nextPageToken,
})
if handleError(ctx, err, w, c.log, "unable to list courses") {
return
}
params := makeTemplListCoursesParams(listCoursesResult.AvailableCoursesOfSub, listCoursesResult.Courses...)
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
if handleError(ctx, err, w, c.log, "unable to list learning types") {
return
}
params.FilterForm.AvailableLearningTypes = xslices.Map(learningTypeResult.LearningTypes, func(in query.LearningType) xtempl.Category {
outcategory := xtempl.Category{
ID: in.ID,
Name: in.Name,
}
if in.ID == pathParams.learningType {
params.FilterForm.ActiveLearningType = outcategory
}
return outcategory
})
if pathParams.learningType != "" {
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
LearningTypeID: pathParams.learningType,
})
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
return
}
params.FilterForm.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) xtempl.Category {
outcategory := xtempl.Category{
ID: in.ID,
Name: in.Name,
}
if pathParams.courseThematic == in.ID {
params.FilterForm.BreadcrumbsParams.ActiveCourseThematic = outcategory
}
return outcategory
})
}
if useBootstrap {
c.log.DebugContext(
ctx, "using bootstrap",
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
)
mapCategory := func(in xtempl.Category) bootstrap.Category {
return bootstrap.Category(in)
}
mapCourseInfo := func(in xtempl.CourseInfo) bootstrap.CourseInfo {
return bootstrap.CourseInfo(in)
}
mapSubcategoryContainer := func(in xtempl.SubcategoryContainer) bootstrap.SubcategoryContainer {
return bootstrap.SubcategoryContainer{
CategoryBaseInfo: bootstrap.CategoryBaseInfo(in.CategoryBaseInfo),
Courses: xslices.Map(in.Courses, mapCourseInfo),
}
}
mapCategoryContainer := func(in xtempl.CategoryContainer) bootstrap.CategoryContainer {
return bootstrap.CategoryContainer{
CategoryBaseInfo: bootstrap.CategoryBaseInfo(in.CategoryBaseInfo),
Subcategories: xslices.Map(in.Subcategories, mapSubcategoryContainer),
}
}
stats := bootstrap.MakeNewStats(0, 0, 0)
params := bootstrap.ListCoursesParams{
FilterForm: bootstrap.FilterFormParams{
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
ActiveLearningType: bootstrap.Category(params.FilterForm.ActiveLearningType),
ActiveCourseThematic: bootstrap.Category(params.FilterForm.ActiveCourseThematic),
},
AvailableLearningTypes: xslices.Map(params.FilterForm.AvailableLearningTypes, mapCategory),
AvailableCourseThematics: xslices.Map(params.FilterForm.AvailableCourseThematics, mapCategory),
},
Categories: xslices.Map(params.Categories, mapCategoryContainer),
}
err = bootstrap.ListCourses(bootstrap.PageCourses, stats, params).Render(ctx, w)
} else {
err = xtempl.ListCourses(stats, params).Render(ctx, w)
}
if handleError(ctx, err, w, c.log, "unable to render list courses") {
return
}
}

View File

@ -1,20 +0,0 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
{{ template "htmlhead" . }}
</head>
<body>
{{ template "htmlbody" . }}
</body>
</html>
{{ end }}
{{ define "htmlhead" }}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .AppName }}</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
{{ end }}

View File

@ -1,14 +0,0 @@
{{ define "htmlbody" }}
{{ template "header" .}}
{{ template "body" .}}
{{ template "footer" .}}
{{ end }}
{{ define "header" }}
{{ end }}
{{ define "body" }}
{{ end }}
{{ define "footer" }}
{{ end }}

View File

@ -1,67 +0,0 @@
package http
import (
"context"
"html/template"
"io/fs"
"log/slog"
"os"
"path"
"git.loyso.art/frx/kurious/internal/common/xcontext"
"git.loyso.art/frx/kurious/internal/common/xslices"
)
const (
baseTemplatePath = "./internal/kurious/ports/http"
templateDir = "/templates"
htmlPath = "/html"
)
func must[T any](t T, err error) T {
if err != nil {
panic(err.Error())
}
return t
}
func scanFiles(dir string) []string {
dst := path.Join(baseTemplatePath, dir)
entries := xslices.Map(
must(os.ReadDir(dst)),
func(v fs.DirEntry) string {
return path.Join(baseTemplatePath, v.Name())
},
)
return entries
}
func getTemplateHTMLBySpecificFiles(ctx context.Context, log *slog.Logger, filenames ...string) *template.Template {
filenames = append([]string{"index.html"}, filenames...)
dir := path.Join(baseTemplatePath, htmlPath)
out := xslices.Map(filenames, func(in string) string {
return path.Join(dir, in)
})
tmpl, err := template.New("courses").ParseFiles(out...)
if err != nil {
xcontext.LogWithWarnError(ctx, log, err, "unable to parse template")
return nil
}
return tmpl
}
func getCoreTemplate(ctx context.Context, log *slog.Logger) *template.Template {
filenames := scanFiles(templateDir)
out, err := template.New("courses").ParseFiles(filenames...)
if err != nil {
xcontext.LogWithWarnError(ctx, log, err, "unable to parse template")
return nil
}
return out
}

View File

@ -5,10 +5,15 @@ import (
stderrors "errors"
"log/slog"
"net/http"
"strconv"
"git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/common/xcontext"
"git.loyso.art/frx/kurious/internal/kurious/service"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"github.com/gorilla/mux"
)
type Server struct {
@ -23,15 +28,7 @@ func NewServer(app service.Application, log *slog.Logger) Server {
}
}
func (s Server) Courses(useTailwind bool) courseServer {
return courseServer{
app: s.app,
log: s.log,
useTailwind: useTailwind,
}
}
func (s Server) CoursesByTempl() courseTemplServer {
func (s Server) Courses() courseTemplServer {
return courseTemplServer(s)
}
@ -40,6 +37,10 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
return false
}
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, "error during handling request")
span.RecordError(err)
var errorString string
var code int
valErr := new(errors.ValidationError)
@ -61,3 +62,55 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
return true
}
type pagination struct {
NextPageToken string
PerPage int
}
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
query := r.URL.Query()
out.NextPageToken = query.Get("next")
if query.Has("per_page") {
out.PerPage, err = strconv.Atoi(query.Get("per_page"))
if err != nil {
return out, errors.NewValidationError("per_page", "bad per_page value")
}
} else {
out.PerPage = 50
}
return out, nil
}
const (
LearningTypePathParam = "learning_type"
ThematicTypePathParam = "thematic_type"
)
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
out.pagination, err = parsePaginationFromQuery(r)
if err != nil {
return out, err
}
vars := mux.Vars(r)
out.LearningType = vars[LearningTypePathParam]
out.CourseThematic = vars[ThematicTypePathParam]
return out, nil
}
type listCoursesParams struct {
pagination
CourseThematic string
LearningType string
}
type IDNamePair struct {
ID string
Name string
IsActive bool
}

View File

@ -1,22 +0,0 @@
package templ
templ button(title string, attributes templ.Attributes) {
<button class="button" { attributes... }>{ title }</button>
}
templ buttonRedirect(id, title string, linkTo string) {
<button
class="button"
id={ "origin-link-" + id }
>
{ title }
</button>
@onclickRedirect("origin-link-" + id, linkTo)
}
script onclickRedirect(id, to string) {
document.getElementById(id).onclick = () => {
location.href = to
}
}

View File

@ -1,116 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.513
package templ
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func button(title string, attributes templ.Attributes) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"button\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, attributes)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/common.templ`, Line: 3, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func buttonRedirect(id, title string, linkTo string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"button\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("origin-link-" + id))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/common.templ`, Line: 11, Col: 8}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = onclickRedirect("origin-link-"+id, linkTo).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func onclickRedirect(id, to string) templ.ComponentScript {
return templ.ComponentScript{
Name: `__templ_onclickRedirect_47ae`,
Function: `function __templ_onclickRedirect_47ae(id, to){document.getElementById(id).onclick = () => {
location.href = to
}}`,
Call: templ.SafeScript(`__templ_onclickRedirect_47ae`, id, to),
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_47ae`, id, to),
}
}

View File

@ -1,62 +0,0 @@
package templ
templ head() {
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Courses Aggregator</title>
<script src="https://unpkg.com/htmx.org@1.8.0"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"/>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/>
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/>
<link rel="manifest" href="/site.webmanifest"/>
</head>
}
templ navigation() {
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
Courses
</div>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item">
Home
</a>
<a class="navbar-item">
Find
</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
More
</a>
<div class="navbar-dropdown">
<a class="navbar-item">
About
</a>
<a class="navbar-item">
Contact
</a>
<hr class="navbar-divider"/>
<a class="navbar-item">
Report an issue
</a>
</div>
</div>
</div>
</div>
</nav>
}
templ footer() {
<footer>
Here will be a footer
</footer>
}

View File

@ -1,182 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.513
package templ
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func head() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var2 := `Courses Aggregator`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><script src=\"https://unpkg.com/htmx.org@1.8.0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var3 := ``
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</script><script src=\"https://unpkg.com/htmx.org/dist/ext/json-enc.js\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var4 := ``
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</script><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css\"><link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\"><link rel=\"manifest\" href=\"/site.webmanifest\"></head>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func navigation() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"navbar\" role=\"navigation\" aria-label=\"main navigation\"><div class=\"navbar-brand\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var6 := `Courses`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><a role=\"button\" class=\"navbar-burger\" aria-label=\"menu\" aria-expanded=\"false\"><span aria-hidden=\"true\"></span> <span aria-hidden=\"true\"></span> <span aria-hidden=\"true\"></span></a><div id=\"navbarBasicExample\" class=\"navbar-menu\"><div class=\"navbar-start\"><a class=\"navbar-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var7 := `Home`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> <a class=\"navbar-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var8 := `Find`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><div class=\"navbar-item has-dropdown is-hoverable\"><a class=\"navbar-link\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var9 := `More`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><div class=\"navbar-dropdown\"><a class=\"navbar-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var10 := `About`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> <a class=\"navbar-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var11 := `Contact`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><hr class=\"navbar-divider\"><a class=\"navbar-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var12 := `Report an issue`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div></div></div></div></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func footer() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var13 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var14 := `Here will be a footer`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -1,209 +0,0 @@
package templ
import "strconv"
import "fmt"
script breadcrumbsLoad() {
const formFilterOnSubmit = event => {
event.preventDefault();
const lt = document.getElementById('learning-type-filter');
const ct = document.getElementById('course-thematic-filter');
const prefix = (lt !== null && lt.value !== '') ? `/courses/${lt.value}` : `/courses`;
const out = (ct !== null && ct.value !== '') ? `${prefix}/${ct.value}` : prefix;
document.location.assign(out);
return false;
};
document.addEventListener('DOMContentLoaded', () => {
const ff = document.getElementById('filter-form');
if (ff === null) return;
ff.addEventListener('submit', formFilterOnSubmit);
});
}
templ breadcrumbItem(enabled bool, link string, isLink bool, title string) {
if enabled {
<li>
<a
if !isEmpty(link) {
href={ templ.SafeURL("/courses" + link) }
itemprop="url"
}
>
<span
itemprop="title"
if isEmpty(link) {
itemprop="url"
}
>
{ title }
</span>
</a>
</li>
}
}
templ listCourseHeader(params FilterFormParams) {
<div class="container block">
@breadcrumb(params.BreadcrumbsParams)
@filterForm(params)
</div>
}
templ breadcrumb(params BreadcrumbsParams) {
<nav
class="breadcrumb"
aria-label="breadcrumbs"
itemprop="breadcrumb"
itemtype="https://schema.org/BreadcrumbList"
itemscope
>
<ul>
@breadcrumbItem(true, "/courses", !isEmpty(params.ActiveLearningType.ID), "Курсы")
@breadcrumbItem(!params.ActiveLearningType.Empty(), "/" + params.ActiveLearningType.ID, !params.ActiveCourseThematic.Empty(), params.ActiveLearningType.Name)
@breadcrumbItem(!params.ActiveCourseThematic.Empty(), "", false, params.ActiveCourseThematic.Name)
</ul>
</nav>
}
templ filterForm(params FilterFormParams) {
<form id="filter-form" class="columns">
<div class="select">
<select id="learning-type-filter" name="learning_type">
<option value="">All learnings</option>
for _, item := range params.AvailableLearningTypes {
<option
value={ item.ID }
if item.ID == params.ActiveLearningType.ID {
selected
}
>
{ item.Name }
</option>
}
</select>
</div>
if !params.ActiveLearningType.Empty() {
<div class="select">
<select id="course-thematic-filter" name="course_thematic">
<option value="">All course thematics</option>
for _, item := range params.AvailableCourseThematics {
<option
value={ item.ID }
if item.ID == params.ActiveCourseThematic.ID {
selected
}
>
{ item.Name }
</option>
}
</select>
</div>
}
@button("goto", templ.Attributes{"id": "go-to-filter"})
</form>
}
templ listCoursesContainer(categories []CategoryContainer) {
<div id="category-course-list" class="container">
for _, category := range categories {
<div class="box">
<div class="title is-3">
<a href={ templ.URL("/courses/" + category.ID) }>{ category.Name }</a>
</div>
<div class="subtitle is-6">
This category contains a lot of interesing courses. Check them out!
</div>
for _, subcategory := range category.Subcategories {
<div class="box">
<div class="title is-4">
<a href={ templ.URL(fmt.Sprintf("/courses/%s/%s", category.ID, subcategory.ID)) }>
{ subcategory.Name }
</a>
<div class="columns is-multiline">
for _, course := range subcategory.Courses {
@courseInfoElement(course)
}
</div>
</div>
</div>
}
</div>
}
</div>
}
templ ListCourses(s stats, params ListCoursesParams) {
@root(s) {
@listCourseHeader(params.FilterForm)
@listCoursesContainer(params.Categories)
<div id="course-info"></div>
}
}
templ root(s stats) {
<!DOCTYPE html>
<html>
@head()
<body>
@navigation()
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">Courses</p>
<p class="title">{ s.CoursesCount }</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Clients</p>
<p class="title">{ s.ClientsCount }</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Categories</p>
<p class="title">{ s.CategoriesCount }</p>
</div>
</div>
</nav>
<div class="section">
{ children... }
</div>
@footer()
@breadcrumbsLoad()
</body>
</html>
}
templ courseInfoElement(params CourseInfo) {
<article class="column is-one-quarter" hx-target="this" hx-swap="outerHTML">
<div class="card">
<div class="card-image">
<figure class="image">
<img src={ params.ImageLink }/>
</figure>
</div>
<div class="card-content">
<div class="media-content">
<p class="title is-5">{ params.Name }</p>
<p class="subtitle is-8">oh well</p>
</div>
<div class="content">
if params.FullPrice > 0 {
<p>{ strconv.Itoa(params.FullPrice) } руб.</p>
} else {
<p>Бесплатно</p>
}
</div>
@buttonRedirect(params.ID, "Show course", params.OriginLink)
</div>
</div>
</article>
}

View File

@ -1,714 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.513
package templ
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "strconv"
import "fmt"
func breadcrumbsLoad() templ.ComponentScript {
return templ.ComponentScript{
Name: `__templ_breadcrumbsLoad_9a1d`,
Function: `function __templ_breadcrumbsLoad_9a1d(){const formFilterOnSubmit = event => {
event.preventDefault();
const lt = document.getElementById('learning-type-filter');
const ct = document.getElementById('course-thematic-filter');
const prefix = (lt !== null && lt.value !== '') ? ` + "`" + `/courses/${lt.value}` + "`" + ` : ` + "`" + `/courses` + "`" + `;
const out = (ct !== null && ct.value !== '') ? ` + "`" + `${prefix}/${ct.value}` + "`" + ` : prefix;
document.location.assign(out);
return false;
};
document.addEventListener('DOMContentLoaded', () => {
const ff = document.getElementById('filter-form');
if (ff === null) return;
ff.addEventListener('submit', formFilterOnSubmit);
});}`,
Call: templ.SafeScript(`__templ_breadcrumbsLoad_9a1d`),
CallInline: templ.SafeScriptInline(`__templ_breadcrumbsLoad_9a1d`),
}
}
func breadcrumbItem(enabled bool, link string, isLink bool, title string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if enabled {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !isEmpty(link) {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL("/courses" + link)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" itemprop=\"url\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("><span itemprop=\"title\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if isEmpty(link) {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" itemprop=\"url\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 41, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func listCourseHeader(params FilterFormParams) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container block\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = breadcrumb(params.BreadcrumbsParams).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = filterForm(params).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func breadcrumb(params BreadcrumbsParams) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"breadcrumb\" aria-label=\"breadcrumbs\" itemprop=\"breadcrumb\" itemtype=\"https://schema.org/BreadcrumbList\" itemscope><ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = breadcrumbItem(true, "/courses", !isEmpty(params.ActiveLearningType.ID), "Курсы").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = breadcrumbItem(!params.ActiveLearningType.Empty(), "/"+params.ActiveLearningType.ID, !params.ActiveCourseThematic.Empty(), params.ActiveLearningType.Name).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = breadcrumbItem(!params.ActiveCourseThematic.Empty(), "", false, params.ActiveCourseThematic.Name).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func filterForm(params FilterFormParams) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form id=\"filter-form\" class=\"columns\"><div class=\"select\"><select id=\"learning-type-filter\" name=\"learning_type\"><option value=\"\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var7 := `All learnings`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range params.AvailableLearningTypes {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(item.ID))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.ID == params.ActiveLearningType.ID {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 83, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !params.ActiveLearningType.Empty() {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"select\"><select id=\"course-thematic-filter\" name=\"course_thematic\"><option value=\"\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var9 := `All course thematics`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range params.AvailableCourseThematics {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(item.ID))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.ID == params.ActiveCourseThematic.ID {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 99, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = button("goto", templ.Attributes{"id": "go-to-filter"}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func listCoursesContainer(categories []CategoryContainer) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"category-course-list\" class=\"container\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, category := range categories {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"box\"><div class=\"title is-3\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 templ.SafeURL = templ.URL("/courses/" + category.ID)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var12)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(category.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 114, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div><div class=\"subtitle is-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var14 := `This category contains a lot of interesing courses. Check them out!`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, subcategory := range category.Subcategories {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"box\"><div class=\"title is-4\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 templ.SafeURL = templ.URL(fmt.Sprintf("/courses/%s/%s", category.ID, subcategory.ID))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var15)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 123, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><div class=\"columns is-multiline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, course := range subcategory.Courses {
templ_7745c5c3_Err = courseInfoElement(course).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func ListCourses(s stats, params ListCoursesParams) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var17 := templ.GetChildren(ctx)
if templ_7745c5c3_Var17 == nil {
templ_7745c5c3_Var17 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var18 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
templ_7745c5c3_Err = listCourseHeader(params.FilterForm).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = listCoursesContainer(params.Categories).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <div id=\"course-info\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = root(s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var18), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func root(s stats) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
if templ_7745c5c3_Var19 == nil {
templ_7745c5c3_Var19 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = head().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = navigation().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"level\"><div class=\"level-item has-text-centered\"><div><p class=\"heading\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var20 := `Courses`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"title\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(s.CoursesCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 158, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div><div class=\"level-item has-text-centered\"><div><p class=\"heading\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var22 := `Clients`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"title\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(s.ClientsCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 164, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div><div class=\"level-item has-text-centered\"><div><p class=\"heading\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var24 := `Categories`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"title\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(s.CategoriesCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 170, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div></nav><div class=\"section\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var19.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = footer().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = breadcrumbsLoad().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func courseInfoElement(params CourseInfo) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
if templ_7745c5c3_Var26 == nil {
templ_7745c5c3_Var26 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<article class=\"column is-one-quarter\" hx-target=\"this\" hx-swap=\"outerHTML\"><div class=\"card\"><div class=\"card-image\"><figure class=\"image\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(params.ImageLink))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"></figure></div><div class=\"card-content\"><div class=\"media-content\"><p class=\"title is-5\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(params.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 193, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"subtitle is-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var28 := `oh well`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div><div class=\"content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if params.FullPrice > 0 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(params.FullPrice))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/list.templ`, Line: 198, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var30 := `руб.`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var31 := `Бесплатно`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = buttonRedirect(params.ID, "Show course", params.OriginLink).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -1,96 +0,0 @@
package templ
import (
"strconv"
"strings"
)
func getCompactedValue(value int) string {
var (
myValue float64
dim string
)
switch {
case value/1e6 > 0:
cutted := value / 1e3
myValue, dim = float64(cutted)/1e3, "m"
case value/1e3 > 0:
myValue, dim = float64(value/1e3), "k"
default:
myValue, dim = float64(value), ""
}
return strings.TrimSuffix(strconv.FormatFloat(myValue, 'f', 3, 32), ".000") + dim
}
func MakeNewStats(courses, clients, categories int) stats {
return stats{
CoursesCount: getCompactedValue(courses),
ClientsCount: getCompactedValue(clients),
CategoriesCount: getCompactedValue(categories),
}
}
type stats struct {
CoursesCount string
ClientsCount string
CategoriesCount string
}
type Category struct {
ID string
Name string
}
func (c Category) Empty() bool {
return c == (Category{})
}
type BreadcrumbsParams struct {
ActiveLearningType Category
ActiveCourseThematic Category
}
type FilterFormParams struct {
BreadcrumbsParams
AvailableLearningTypes []Category
AvailableCourseThematics []Category
}
func isEmpty(s string) bool {
return s == ""
}
type CourseInfo struct {
ID string
Name string
FullPrice int
ImageLink string
OriginLink string
}
type CategoryBaseInfo struct {
ID string
Name string
Description string
Count int
}
type CategoryContainer struct {
CategoryBaseInfo
Subcategories []SubcategoryContainer
}
type SubcategoryContainer struct {
CategoryBaseInfo
Courses []CourseInfo
}
type ListCoursesParams struct {
FilterForm FilterFormParams
Categories []CategoryContainer
}

View File

@ -1,57 +0,0 @@
package templ
import "testing"
func TestGetCompactedValue(t *testing.T) {
var tt = []struct {
name string
in int
exp string
}{
{
name: "less than 1k",
in: 666,
exp: "666",
},
{
name: "exactly 1k",
in: 1000,
exp: "1k",
},
{
name: "some thousands",
in: 12345,
exp: "12k",
},
{
name: "more thousands",
in: 123456,
exp: "123k",
},
{
name: "million",
in: 1e6,
exp: "1m",
},
{
name: "some millions",
in: 2e6,
exp: "2m",
},
{
name: "more complex value",
in: 1.2346e6,
exp: "1.234m",
},
}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got := getCompactedValue(tc.in)
if tc.exp != got {
t.Errorf("exp=%s got=%s", tc.exp, got)
}
})
}
}

View File

@ -1,73 +0,0 @@
{{ define "html_head" }}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Courses Aggregator</title>
<script src="https://unpkg.com/htmx.org@1.8.0"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
</head>
{{ end }}
{{ define "header" }}
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
Courses
</div>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item">
Home
</a>
<a class="navbar-item">
Find
</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
More
</a>
<div class="navbar-dropdown">
<a class="navbar-item">
About
</a>
<a class="navbar-item">
Contact
</a>
<hr class="navbar-divider">
<a class="navbar-item">
Report an issue
</a>
</div>
</div>
</div>
</div>
</nav>
{{ end }}
{{ define "footer" }}
<footer>
Here will be footer
</footer>
{{ end }}

View File

@ -1,81 +0,0 @@
{{ define "course_info" }}
<article class="column is-one-quarter" hx-target="this" hx-swap="outerHTML">
<div class="card">
<div class="card-image">
<figure class="image">
<img src="{{ .ImageLink }}">
</figure>
</div>
<div class="card-content">
<div class="media-content">
<p class="title is-5" onclick="location.href='{{ .OriginLink }}'">{{ .Name }}</p>
<p class="subtitle is-8">{{ .Description }}</p>
</div>
<div class="content">
{{ if .FullPrice }}
<p>{{ .FullPrice }} rub.</p>
{{ else }}
<p>Бесплатно</p>
{{ end }}
</div>
<button class="button" onclick="location.href='{{ .OriginLink }}'">
Show Course
</button>
<button class="button" hx-get="/courses/{{ .ID }}/editdesc">
Edit description
</button>
<!-- <button class="button" hx-get="/courses/{{ .ID }}/" hx-target="#course-info" hx-swap="innerHTML">
View course
</button> -->
</div>
</div>
</article>
{{ end }}
{{ define "edit_description" }}
<form
hx-ext="json-enc"
hx-put="/courses/{{ .ID }}/description"
hx-target="this"
hx-swap="outerHTML"
>
<fieldset disabled>
<div class="field">
<label class="label">Name</label>
<div class="control">
<input class="input" type="text" placeholder="Text input" value="{{ .Name }}">
</div>
</fieldset>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Description</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<textarea class="textarea" name="description" placeholder="Description">{{ .Description }}</textarea>
</div>
</div>
</div>
</div>
<div>
<p>Full price: {{ .FullPrice }}</p>
</div>
<div class="field is-grouped">
<p class="control">
<button class="button is-primary is-link" hx-include="[name='description']">
Submit
</button>
</p>
<p class="control">
<button class="button is-light" hx-get="/courses/{{ .ID }}/short">
Cancel
</button>
</p>
</div>
</form>
{{ end }}

View File

@ -1,185 +0,0 @@
{{define "courses"}}
<!DOCTYPE html>
<html>
{{ template "html_head" . }}
<body>
{{ template "header" . }}
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">Courses</p>
<p class="title">10k</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Clients</p>
<p class="title">1m</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Categories</p>
<p class="title">1,024</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Likes</p>
<p class="title">Over 9m</p>
</div>
</div>
</nav>
<div class="section">
<div class="container block">
<nav class="breadcrumb" aria-label="breadcrumbs" itemprop="breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">
<ul>
<li>
{{ if .LearningTypeName }}
<a href="/courses" itemprop="url">
<span itemprop="title">
Курсы
</span>
</a>
{{ else }}
<a>
<span itemprop="title" itemprop="url">
Курсы
</span>
</a>
{{ end }}
</li>
{{ if .LearningTypeName }}
<li>
{{ if .CourseThematicName }}
<a href="/courses/{{.ActiveLearningType}}" itemprop="url">
<span itemprop="title">
{{ .LearningTypeName }}
</span>
</a>
{{ else }}
<a>
<span itemprop="title" itemprop="url">
{{ .LearningTypeName }}
</span>
</a>
{{ end }}
</li>
{{ end }}
{{ if .CourseThematicName }}
<li>
<a>
<span itemprop="title" itemprop="url">
{{ .CourseThematicName }}
</span>
</a>
</li>
{{ end }}
</ul>
</nav>
<form id="filter-form" class="columns">
<div class="select">
<select id="learning-type-filter" name="learning_type">
<option value="">Все направления</option>
{{ range $t := .AvailableLearningTypes }}
<option value="{{$t.ID}}" {{ if eq $t.ID $.ActiveLearningType }}selected{{ end }}>{{ $t.Name }}</option>
{{ end }}
</select>
</div>
{{ if .LearningTypeName }}
<div class="select">
<select id="course-thematic-filter" name="course_thematic">
<option value="">Все темы</option>
{{ range $t := .AvailableCourseThematics }}
<option value="{{$t.ID}}" {{ if eq $t.ID $.ActiveCourseThematic }}selected{{ end }}>{{ $t.Name }}</option>
{{ end }}
</select>
</div>
{{ end }}
<button id="go-to-filter" class="button">Перейти</button>
</form>
</div>
<div id="category-course-list" class="container">
{{ range $category := .Categories }}
<div class="box">
<div class="title is-3">
<a href="/courses/{{ $category.ID }}">
{{ $category.Name }}
</a>
</div>
<div class="subtitle is-6">
Some description about the learning category {{ $category.Description }}
</div>
{{ range $subcategory := $category.Subcategories }}
<div class="box">
<div class="title is-4">
<a href="/courses/{{ $category.ID }}/{{ $subcategory.ID }}">
{{ $subcategory.Name }}
</a>
</div>
<div class="subtitle is-6">Some description about course thematics {{ $subcategory.Description }}</div>
<div class="columns is-multiline">
{{ range $course := $subcategory.Courses }}
{{ template "course_info" $course }}
{{ end }}
</div>
</div>
{{ end }}
</div>
{{ end }}
</div>
<div id="course-info"></div>
</div>
</div>
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
<a class="pagination-previous">Previous</a>
<a class="pagination-next" href="/courses/?next={{ .NextPageToken }}&per_page=50">Next page</a>
</nav>
{{ template "footer" . }}
<script>
const formFilterOnSubmit = event => {
event.preventDefault();
const lt = document.getElementById('learning-type-filter')
const ct = document.getElementById('course-thematic-filter');
let out = '/courses';
if (lt != null && lt.value != '') {
out += '/' + lt.value;
}
if (ct != null && ct.value != '') {
out += '/' + ct.value;
}
document.location.assign(out);
return false;
};
document.addEventListener('DOMContentLoaded', () => {
const ff = document.getElementById('filter-form');
ff.addEventListener('submit', formFilterOnSubmit);
})
</script>
</body>
</html>
{{end}}