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

@ -9,9 +9,12 @@ import (
)
type Config struct {
Log config.Log `json:"log"`
YDB config.YDB `json:"ydb"`
HTTP config.HTTP `json:"http"`
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,208 +1,152 @@
<!doctype html>
<html lang="en">
<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"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
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"
>
<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" href="/courses.html">Courses</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"
>
<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>
</ol>
</nav>
<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"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
</head>
<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=""
/>
<div class="card-body">
<h5 class="card-title">
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 ...
</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
>
<small class="text-body-secondary"></small>
</div>
<div class="card-footer text-end">
<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=""
/>
<div class="card-body">
<h5 class="card-title">
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 ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<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.
</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 ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<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.
</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 ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<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.
</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 ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
</div>
<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">
<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" href="/courses.html">Courses</a>
<a class="nav-link active" href="/core.html">About us</a>
</div>
</body>
</div>
</div>
</nav>
<div class="container">
<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>
</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="" />
<div class="card-body">
<h5 class="card-title">
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 ...
</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>
<small class="text-body-secondary"></small>
</div>
<div class="card-footer text-end">
<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="" />
<div class="card-body">
<h5 class="card-title">
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 ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<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.
</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 ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<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.
</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 ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<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.
</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 ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,206 +1,142 @@
<!doctype html>
<html lang="en">
<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"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"
></script>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<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"
>
<span class="navbar-toggler-icon"></span>
</button>
<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
>
</li>
<li class="nav-item">
<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
>
</li>
</ul>
</div>
</div>
</nav>
</header>
<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"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<div class="container">
<section class="row header">
<nav
class="mt-4"
style="--bs-breadcrumb-divider: &quot;/&quot;"
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>
</ol>
</nav>
</section>
<section class="row filters">
<div class="col-8">
<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"
>
<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"
>
<option selected>All</option>
<option value="1">Web development</option>
<option value="2">Backend</option>
<option value="3">Frontend</option>
</select>
<button class="btn btn-outline-secondary" type="button">
> Go
</button>
</div>
</div>
</section>
<section class="row first-class-group">
<h1 class="title">Languages</h1>
<p>
A languages category provides all courses to help learn
language
</p>
<div class="filter-content d-flex mb-3">
<div class="p-2">
<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">
Second but not the shortest named school
</option>
<option value="3">Third small</option>
</select>
</div>
<div class="p-2">
<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>
<option value="3">Three</option>
<option value="4">Threerrrrrrrrrrrrrrrrrr</option>
</select>
</div>
<div class="ms-auto p-2">
<div class="btn btn-primary">Promocodes</div>
</div>
</div>
<div class="second-class-group block">
<h2 class="title">Japanese</h2>
<p>Looking for a course to learn japanese language?</p>
<div class="row g-4">
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<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>
<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
>
</div>
</div>
</div>
</div>
</div>
</div>
<hr />
</section>
<footer class="row">
<div class="text-end">
<p>(c) All right reserved</p>
</div>
</footer>
<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">
<span class="navbar-toggler-icon"></span>
</button>
<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>
</li>
<li class="nav-item">
<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>
</li>
</ul>
</div>
</body>
</div>
</nav>
</header>
<div class="container">
<section class="row header">
<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>
</ol>
</nav>
</section>
<section class="row filters">
<div class="col-8">
<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">
<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">
<option selected>All</option>
<option value="1">Web development</option>
<option value="2">Backend</option>
<option value="3">Frontend</option>
</select>
<button class="btn btn-outline-secondary" type="button">
> Go
</button>
</div>
</div>
</section>
<section class="row first-class-group">
<h1 class="title">Languages</h1>
<p>A languages category provides all courses to help learn language</p>
<div class="filter-content d-flex mb-3">
<div class="p-2">
<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">
Second but not the shortest named school
</option>
<option value="3">Third small</option>
</select>
</div>
<div class="p-2">
<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>
<option value="3">Three</option>
<option value="4">Threerrrrrrrrrrrrrrrrrr</option>
</select>
</div>
<div class="ms-auto p-2">
<div class="btn btn-primary">Promocodes</div>
</div>
</div>
<div class="second-class-group block">
<h2 class="title">Japanese</h2>
<p>Looking for a course to learn japanese language?</p>
<div class="row g-4">
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<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>
<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>
</div>
</div>
</div>
</div>
</div>
</div>
<hr />
</section>
<footer class="row">
<div class="text-end">
<p>(c) All right reserved</p>
</div>
</footer>
</div>
</body>
</html>

View File

@ -1,126 +1,183 @@
<!doctype html>
<html lang="en">
<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"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"
></script>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<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"
>
<span class="navbar-toggler-icon"></span>
</button>
<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
>
</li>
<li class="nav-item">
<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
>
</li>
</ul>
</div>
</div>
</nav>
</header>
<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"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<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>
</div>
<div class="row categories">
<div class="col">
<div class="block">
<ul>
<p>Category 1</p>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="block">
<ul>
<p>Category 2</p>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="block">
<ul>
<p>Category 3</p>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
</ul>
</div>
</div>
</div>
<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">
<span class="navbar-toggler-icon"></span>
</button>
<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>
</li>
<li class="nav-item">
<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>
</li>
</ul>
</div>
</body>
</div>
</nav>
</header>
<div class="container">
<div class="row upper mb-4 text-center" style="min-height: 4rem">
<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">
<div class="col">
<div class="block">
<ul>
<p>Category 1</p>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="block">
<ul>
<p>Category 2</p>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="block">
<ul>
<p>Category 3</p>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
<li>
<div class="block">item</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</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

@ -4,31 +4,31 @@ import "path"
import "strconv"
script breadcrumbsLoad() {
const formFilterOnSubmit = event => {
event.preventDefault();
const formFilterOnSubmit = event => {
event.preventDefault();
const lt = document.getElementById('learning-type-filter');
const ct = document.getElementById('course-thematic-filter');
const lt = document.getElementById('learning-type-filter');
const ct = document.getElementById('course-thematic-filter');
const prefix = (lt !== null && lt.value !== '') ? `/courses/${lt.value}` : `/courses`;
const out = (ct !== null && ct.value !== '') ? `${prefix}/${ct.value}` : prefix;
const prefix = (lt !== null && lt.value !== '') ? `/courses/${lt.value}` : `/courses`;
const out = (ct !== null && ct.value !== '') ? `${prefix}/${ct.value}` : prefix;
document.location.assign(out);
return false;
};
document.location.assign(out);
return false;
};
document.addEventListener('DOMContentLoaded', () => {
const ff = document.getElementById('filter-form');
if (ff === null) return;
ff.addEventListener('submit', formFilterOnSubmit);
});
document.addEventListener('DOMContentLoaded', () => {
const ff = document.getElementById('filter-form');
if (ff === null) return;
ff.addEventListener('submit', formFilterOnSubmit);
});
}
templ breadcrumbsItem(text, link string, isActive bool) {
<li class={"breadcrumb-item", templ.KV("active", isActive)}>
<li class={ "breadcrumb-item", templ.KV("active", isActive) }>
if link != "" {
<a
href={templ.URL(link)}
href={ templ.URL(link) }
itemprop="url"
aria-label="breadcrumb"
>{ text }</a>
@ -39,9 +39,8 @@ templ breadcrumbsItem(text, link string, isActive bool) {
}
templ breadcrumNode(params BreadcrumbsParams) {
// TODO: add divider to nav style
<nav
class={"mt-4", breadcrumbSymbol()}
class={ "mt-4", breadcrumbSymbol() }
aria-label="breadcrumbs"
itemprop="breadcrumb"
itemtype="https://schema.org/BreadcrumbList"
@ -49,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,30 +139,29 @@ css myImg() {
min-width: 19rem;
}
css cardTextSize() {
min-height: 12rem;
}
templ listCoursesCard(info CourseInfo) {
// <div class="col-12 col-md-6 col-lg-3">
<div class="col">
<div class="card h-100">
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128")} alt="Course picture" class={"card-img-top"}/>
<div class={"card-body", cardTextSize(), "row"}>
<h5 class="card-title">{info.Name}</h5>
<div class="input-group d-flex align-self-end">
<a
href={ templ.URL(info.OriginLink) }
class="btn text btn-outline-primary flex-grow-1"
>Go!</a>
<span class="input-group-text justify-content-end flex-fill">
{strconv.Itoa(info.FullPrice)} rub.
</span>
</div>
// <div class="col-12 col-md-6 col-lg-3">
<div class="col">
<div class="card h-100">
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128") } alt="Course picture" class={ "card-img-top" }/>
<div class={ "card-body", cardTextSize(), "row" }>
<h5 class="card-title">{ info.Name }</h5>
<div class="input-group d-flex align-self-end">
<a
href={ templ.URL(info.OriginLink) }
class="btn text btn-outline-primary flex-grow-1"
>Go!</a>
<span class="input-group-text justify-content-end flex-fill">
{ strconv.Itoa(info.FullPrice) } rub.
</span>
</div>
</div>
</div>
</div>
}
templ ListCourses(pageType PageKind, s stats, params ListCoursesParams) {

View File

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

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

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