add opentelemetry tracing
This commit is contained in:
@ -1 +1 @@
|
||||
3c1808b7a88ab24b1cacf9a132073105
|
||||
3cf236a901d03e42352790df844d58c5
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
println("oh well")
|
||||
}
|
||||
@ -11,7 +11,10 @@ import (
|
||||
type Config struct {
|
||||
Log config.Log `json:"log"`
|
||||
YDB config.YDB `json:"ydb"`
|
||||
Sqlite config.Sqlite `json:"sqlite"`
|
||||
HTTP config.HTTP `json:"http"`
|
||||
Tracing config.Trace `json:"tracing"`
|
||||
DBEngine string `json:"db_engine"`
|
||||
}
|
||||
|
||||
func readFromFile(path string, defaultConfigF func() Config) (Config, error) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
132
cmd/kuriweb/trace.go
Normal 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
35
go.mod
@ -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
80
go.sum
@ -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=
|
||||
|
||||
@ -1,121 +1,82 @@
|
||||
<!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"
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body data-bs-theme="dark">
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Kurious</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse" id="navbarNavAltMarkup">
|
||||
<div class="navbar-nav">
|
||||
<a
|
||||
class="nav-link"
|
||||
aria-current="page"
|
||||
href="/index.html"
|
||||
>Home</a
|
||||
>
|
||||
<a class="nav-link" aria-current="page" href="/index.html">Home</a>
|
||||
<a class="nav-link" href="/courses.html">Courses</a>
|
||||
<a class="nav-link active" href="/core.html"
|
||||
>About us</a
|
||||
>
|
||||
<a class="nav-link active" href="/core.html">About us</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<nav
|
||||
style="--bs-breadcrumb-divider: ">""
|
||||
aria-label="breadcrumb"
|
||||
>
|
||||
<nav style="--bs-breadcrumb-divider: '>'" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="#">Home</a></li>
|
||||
<li class="breadcrumb-item" aria-current="page">
|
||||
<a href="#">Course</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
Theme
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Theme</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img
|
||||
src="https://placehold.co/128x128"
|
||||
class="card-img-top"
|
||||
alt=""
|
||||
/>
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur
|
||||
adipisicing elit.
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur
|
||||
adipisicing elit. Enim omnis vero, reiciendis
|
||||
obcaecati perferendis excepturi nostrum nobis
|
||||
itaque modi dignissimos ...
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<!-- <a href="#" class="btn btn-primary">Go somewhere</a> -->
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<a href="#" class="btn btn-primary"
|
||||
>Buy for 399.99$</a
|
||||
>
|
||||
<a href="#" class="btn btn-primary">Buy for 399.99$</a>
|
||||
<small class="text-body-secondary"></small>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<small class="text-body-secondary col"
|
||||
>399.99$</small
|
||||
>
|
||||
<small class="text-body-secondary col">399.99$</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img
|
||||
src="https://placehold.co/128x128"
|
||||
class="card-img-top"
|
||||
alt=""
|
||||
/>
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur
|
||||
adipisicing elit.
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur
|
||||
adipisicing elit. Enim omnis vero, reiciendis
|
||||
obcaecati perferendis excepturi nostrum nobis
|
||||
itaque modi dignissimos ...
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
@ -127,21 +88,15 @@
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img
|
||||
src="https://placehold.co/128x128"
|
||||
class="card-img-top"
|
||||
alt=""
|
||||
/>
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur
|
||||
adipisicing elit.
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur
|
||||
adipisicing elit. Enim omnis vero, reiciendis
|
||||
obcaecati perferendis excepturi nostrum nobis
|
||||
itaque modi dignissimos ...
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
@ -153,21 +108,15 @@
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img
|
||||
src="https://placehold.co/128x128"
|
||||
class="card-img-top"
|
||||
alt=""
|
||||
/>
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur
|
||||
adipisicing elit.
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur
|
||||
adipisicing elit. Enim omnis vero, reiciendis
|
||||
obcaecati perferendis excepturi nostrum nobis
|
||||
itaque modi dignissimos ...
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
@ -179,21 +128,15 @@
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img
|
||||
src="https://placehold.co/128x128"
|
||||
class="card-img-top"
|
||||
alt=""
|
||||
/>
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur
|
||||
adipisicing elit.
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur
|
||||
adipisicing elit. Enim omnis vero, reiciendis
|
||||
obcaecati perferendis excepturi nostrum nobis
|
||||
itaque modi dignissimos ...
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
@ -205,4 +148,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
<!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"
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
</head>
|
||||
|
||||
@ -23,42 +18,20 @@
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/index.html">Kurious</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div
|
||||
class="navbar-collapse collapse"
|
||||
id="navbarSupportedContent"
|
||||
>
|
||||
<div class="navbar-collapse collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mb-lg-0 mb-2 me-auto">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
aria-current="page"
|
||||
href="/index.html"
|
||||
>Home</a
|
||||
>
|
||||
<a class="nav-link" aria-current="page" href="/index.html">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link active"
|
||||
aria-current="page"
|
||||
href="/courses.html"
|
||||
>Courses</a
|
||||
>
|
||||
<a class="nav-link active" aria-current="page" href="/courses.html">Courses</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/core.html"
|
||||
>About us</a
|
||||
>
|
||||
<a class="nav-link" href="/core.html">About us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -68,19 +41,13 @@
|
||||
|
||||
<div class="container">
|
||||
<section class="row header">
|
||||
<nav
|
||||
class="mt-4"
|
||||
style="--bs-breadcrumb-divider: "/""
|
||||
aria-label="breadcrumb"
|
||||
>
|
||||
<nav class="mt-4" style="--bs-breadcrumb-divider: '/'" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="#">Main</a></li>
|
||||
<li class="breadcrumb-item" aria-current="page">
|
||||
<a href="#">Languages</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
Japanese
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Japanese</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</section>
|
||||
@ -90,22 +57,14 @@
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Filter categories</span>
|
||||
|
||||
<select
|
||||
class="form-select"
|
||||
id="inputGroupSelect04"
|
||||
aria-label="Example select with button addon"
|
||||
>
|
||||
<select class="form-select" id="inputGroupSelect04" aria-label="Example select with button addon">
|
||||
<option selected>All</option>
|
||||
<option value="1">Programming</option>
|
||||
<option value="2">Design</option>
|
||||
<option value="3">Business</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
class="form-select"
|
||||
id="inputGroupSelect04"
|
||||
aria-label="Example select with button addon"
|
||||
>
|
||||
<select class="form-select" id="inputGroupSelect04" aria-label="Example select with button addon">
|
||||
<option selected>All</option>
|
||||
<option value="1">Web development</option>
|
||||
<option value="2">Backend</option>
|
||||
@ -120,18 +79,11 @@
|
||||
|
||||
<section class="row first-class-group">
|
||||
<h1 class="title">Languages</h1>
|
||||
<p>
|
||||
A languages category provides all courses to help learn
|
||||
language
|
||||
</p>
|
||||
<p>A languages category provides all courses to help learn language</p>
|
||||
|
||||
<div class="filter-content d-flex mb-3">
|
||||
<div class="p-2">
|
||||
<select
|
||||
class="form-select"
|
||||
id="inputGroupSelect04"
|
||||
aria-label="Example select with button addon"
|
||||
>
|
||||
<select class="form-select" id="inputGroupSelect04" aria-label="Example select with button addon">
|
||||
<option selected>Pick a school</option>
|
||||
<option value="1">First school in the row</option>
|
||||
<option value="2">
|
||||
@ -142,11 +94,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-2">
|
||||
<select
|
||||
class="form-select"
|
||||
id="inputGroupSelect04"
|
||||
aria-label="Example select with button addon"
|
||||
>
|
||||
<select class="form-select" id="inputGroupSelect04" aria-label="Example select with button addon">
|
||||
<option selected>Sort by</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
@ -167,25 +115,12 @@
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<img
|
||||
src="https://placehold.co/128x128"
|
||||
class="card-img-top"
|
||||
alt="..."
|
||||
/>
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="..." />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Card title with a long naming
|
||||
</h5>
|
||||
<h5 class="card-title">Card title with a long naming</h5>
|
||||
<div class="input-group d-flex">
|
||||
<a
|
||||
href="#"
|
||||
class="btn text btn-outline-primary flex-grow-1"
|
||||
>Open ></a
|
||||
>
|
||||
<span
|
||||
class="input-group-text justify-content-end flex-fill"
|
||||
>500$</span
|
||||
>
|
||||
<a href="#" class="btn text btn-outline-primary flex-grow-1">Open ></a>
|
||||
<span class="input-group-text justify-content-end flex-fill">500$</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -203,4 +138,5 @@
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
<!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"
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
</head>
|
||||
|
||||
@ -23,42 +18,20 @@
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/index.html">Kurious</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div
|
||||
class="navbar-collapse collapse"
|
||||
id="navbarSupportedContent"
|
||||
>
|
||||
<div class="navbar-collapse collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mb-lg-0 mb-2 me-auto">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link active"
|
||||
aria-current="page"
|
||||
href="/index.html"
|
||||
>Home</a
|
||||
>
|
||||
<a class="nav-link active" aria-current="page" href="/index.html">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
aria-current="page"
|
||||
href="/courses.html"
|
||||
>Courses</a
|
||||
>
|
||||
<a class="nav-link" aria-current="page" href="/courses.html">Courses</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/core.html"
|
||||
>About us</a
|
||||
>
|
||||
<a class="nav-link" href="/core.html">About us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -68,7 +41,90 @@
|
||||
|
||||
<div class="container">
|
||||
<div class="row upper mb-4 text-center" style="min-height: 4rem">
|
||||
<p class="justify-content-center">Some header about courses</p>
|
||||
<p class="justify-content-center">Here you can find course for any taste</p>
|
||||
</div>
|
||||
|
||||
<div class="container w-75">
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<div class="col-12 col-md-8 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Programming</h5>
|
||||
<hr/>
|
||||
<p>In this category you can find courses of types such as</p>
|
||||
<p>web-development, backend development, frontend developent</p>
|
||||
<p>This category contains <span>128</span> courses.</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">128 items</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Programming</h5>
|
||||
<hr/>
|
||||
<p>In this category you can find courses of types such as</p>
|
||||
<p>web-development, backend development, frontend developent</p>
|
||||
<p>This category contains <span>128</span> courses.</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">128 items</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Programming</h5>
|
||||
<hr/>
|
||||
<p>In this category you can find courses of types such as</p>
|
||||
<p>web-development, backend development, frontend developent</p>
|
||||
<p>This category contains <span>128</span> courses.</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">128 items</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Programming</h5>
|
||||
<hr/>
|
||||
<p>In this category you can find courses of types such as</p>
|
||||
<p>web-development, backend development, frontend developent</p>
|
||||
<p>This category contains <span>128</span> courses.</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">128 items</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row categories">
|
||||
@ -123,4 +179,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -3,5 +3,4 @@ package config
|
||||
type HTTP struct {
|
||||
ListenAddr string `json:"listen_addr"`
|
||||
MountLive bool `json:"mount_live"`
|
||||
Engine string `json:"engine"`
|
||||
}
|
||||
|
||||
6
internal/common/config/trace.go
Normal file
6
internal/common/config/trace.go
Normal file
@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
type Trace struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
LicenseKey string `json:"license_key"`
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,6 @@ templ breadcrumbsItem(text, link string, isActive bool) {
|
||||
}
|
||||
|
||||
templ breadcrumNode(params BreadcrumbsParams) {
|
||||
// TODO: add divider to nav style
|
||||
<nav
|
||||
class={ "mt-4", breadcrumbSymbol() }
|
||||
aria-label="breadcrumbs"
|
||||
@ -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,7 +81,6 @@ 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" }
|
||||
@ -97,7 +93,6 @@ templ listCoursesSectionFilters(params FilterFormParams) {
|
||||
>{ learningType.Name }</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<select
|
||||
id="course-thematic-filter"
|
||||
class={ "form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0) }
|
||||
@ -120,7 +115,6 @@ templ listCoursesLearning(containers []CategoryContainer) {
|
||||
for _, container := range containers {
|
||||
<section class="row first-class-group">
|
||||
<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>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
for _, info := range subcategory.Courses {
|
||||
@listCoursesCard(info)
|
||||
@ -147,7 +139,6 @@ css myImg() {
|
||||
min-width: 19rem;
|
||||
}
|
||||
|
||||
|
||||
css cardTextSize() {
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ import "strconv"
|
||||
|
||||
func breadcrumbsLoad() templ.ComponentScript {
|
||||
return templ.ComponentScript{
|
||||
Name: `__templ_breadcrumbsLoad_9a1d`,
|
||||
Function: `function __templ_breadcrumbsLoad_9a1d(){const formFilterOnSubmit = event => {
|
||||
Name: `__templ_breadcrumbsLoad_e656`,
|
||||
Function: `function __templ_breadcrumbsLoad_e656(){const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
const lt = document.getElementById('learning-type-filter');
|
||||
@ -35,8 +35,8 @@ func breadcrumbsLoad() templ.ComponentScript {
|
||||
if (ff === null) return;
|
||||
ff.addEventListener('submit', formFilterOnSubmit);
|
||||
});}`,
|
||||
Call: templ.SafeScript(`__templ_breadcrumbsLoad_9a1d`),
|
||||
CallInline: templ.SafeScriptInline(`__templ_breadcrumbsLoad_9a1d`),
|
||||
Call: templ.SafeScript(`__templ_breadcrumbsLoad_e656`),
|
||||
CallInline: templ.SafeScriptInline(`__templ_breadcrumbsLoad_e656`),
|
||||
}
|
||||
}
|
||||
|
||||
@ -322,7 +322,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(learningType.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 96, Col: 25}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 92, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -399,7 +399,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 109, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 104, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -451,7 +451,7 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(container.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 121, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 116, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -499,7 +499,7 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 133, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 126, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -517,7 +517,7 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 134, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 127, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -535,7 +535,7 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(subcategory.Count))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 134, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 127, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -654,7 +654,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 160, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 151, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -685,7 +685,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 167, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 158, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
58
internal/kurious/ports/http/bootstrap/main.templ
Normal file
58
internal/kurious/ports/http/bootstrap/main.templ
Normal 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)
|
||||
}
|
||||
}
|
||||
207
internal/kurious/ports/http/bootstrap/main_templ.go
Normal file
207
internal/kurious/ports/http/bootstrap/main_templ.go
Normal 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
|
||||
})
|
||||
}
|
||||
@ -2,352 +2,267 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/command"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/ports/http/bootstrap"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type courseServer struct {
|
||||
var (
|
||||
paramsAttr = attribute.Key("params")
|
||||
|
||||
webtracer = otel.Tracer("http")
|
||||
)
|
||||
|
||||
type courseTemplServer struct {
|
||||
app service.Application
|
||||
log *slog.Logger
|
||||
|
||||
useTailwind bool
|
||||
}
|
||||
|
||||
type pagination struct {
|
||||
nextPageToken string
|
||||
perPage int
|
||||
}
|
||||
|
||||
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
||||
query := r.URL.Query()
|
||||
out.nextPageToken = query.Get("next")
|
||||
|
||||
if query.Has("per_page") {
|
||||
out.perPage, err = strconv.Atoi(query.Get("per_page"))
|
||||
if err != nil {
|
||||
return out, errors.NewValidationError("per_page", "bad per_page value")
|
||||
}
|
||||
} else {
|
||||
out.perPage = 50
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
const (
|
||||
LearningTypePathParam = "learning_type"
|
||||
ThematicTypePathParam = "thematic_type"
|
||||
)
|
||||
|
||||
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
||||
out.pagination, err = parsePaginationFromQuery(r)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
out.learningType = vars[LearningTypePathParam]
|
||||
out.courseThematic = vars[ThematicTypePathParam]
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type listCoursesParams struct {
|
||||
pagination
|
||||
|
||||
courseThematic string
|
||||
learningType string
|
||||
}
|
||||
|
||||
type baseInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
type categoryInfo struct {
|
||||
baseInfo
|
||||
|
||||
Subcategories []subcategoryInfo
|
||||
}
|
||||
|
||||
type subcategoryInfo struct {
|
||||
baseInfo
|
||||
|
||||
Courses []domain.Course
|
||||
}
|
||||
|
||||
type IDNamePair struct {
|
||||
ID string
|
||||
Name string
|
||||
IsActive bool
|
||||
}
|
||||
|
||||
type listCoursesTemplateParams struct {
|
||||
Categories []categoryInfo
|
||||
NextPageToken string
|
||||
AvailableLearningTypes []IDNamePair
|
||||
AvailableCourseThematics []IDNamePair
|
||||
|
||||
ActiveLearningType string
|
||||
LearningTypeName string
|
||||
|
||||
ActiveCourseThematic string
|
||||
CourseThematicName string
|
||||
}
|
||||
|
||||
func mapDomainCourseToTemplate(in ...domain.Course) listCoursesTemplateParams {
|
||||
coursesBySubcategory := make(map[string][]domain.Course, len(in))
|
||||
func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) bootstrap.ListCoursesParams {
|
||||
coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in))
|
||||
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
||||
categoryByID := make(map[string]baseInfo, len(in))
|
||||
categoryByID := make(map[string]bootstrap.CategoryBaseInfo, len(in))
|
||||
|
||||
xslices.ForEach(in, func(c domain.Course) {
|
||||
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], c)
|
||||
courseInfo := bootstrap.CourseInfo{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
FullPrice: int(c.FullPrice),
|
||||
ImageLink: c.ImageLink,
|
||||
OriginLink: c.OriginLink,
|
||||
}
|
||||
|
||||
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], courseInfo)
|
||||
|
||||
if _, ok := subcategoriesByCategories[c.LearningTypeID]; !ok {
|
||||
subcategoriesByCategories[c.LearningTypeID] = map[string]struct{}{}
|
||||
}
|
||||
subcategoriesByCategories[c.LearningTypeID][c.ThematicID] = struct{}{}
|
||||
|
||||
if _, ok := categoryByID[c.LearningTypeID]; !ok {
|
||||
categoryByID[c.LearningTypeID] = baseInfo{
|
||||
categoryByID[c.LearningTypeID] = bootstrap.CategoryBaseInfo{
|
||||
ID: c.LearningTypeID,
|
||||
Name: c.LearningType,
|
||||
}
|
||||
}
|
||||
if _, ok := categoryByID[c.ThematicID]; !ok {
|
||||
categoryByID[c.ThematicID] = baseInfo{
|
||||
categoryByID[c.ThematicID] = bootstrap.CategoryBaseInfo{
|
||||
ID: c.ThematicID,
|
||||
Name: c.Thematic,
|
||||
Count: counts[c.ThematicID],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var out listCoursesTemplateParams
|
||||
for category, subcategoryMap := range subcategoriesByCategories {
|
||||
outCategory := categoryInfo{
|
||||
baseInfo: categoryByID[category],
|
||||
var out bootstrap.ListCoursesParams
|
||||
for categoryID, subcategoriesID := range subcategoriesByCategories {
|
||||
outCategory := bootstrap.CategoryContainer{
|
||||
CategoryBaseInfo: categoryByID[categoryID],
|
||||
}
|
||||
|
||||
for subcategory := range subcategoryMap {
|
||||
outSubCategory := subcategoryInfo{
|
||||
baseInfo: categoryByID[subcategory],
|
||||
Courses: coursesBySubcategory[subcategory],
|
||||
for subcategoryID := range subcategoriesID {
|
||||
outSubcategory := bootstrap.SubcategoryContainer{
|
||||
CategoryBaseInfo: categoryByID[subcategoryID],
|
||||
Courses: coursesBySubcategory[subcategoryID],
|
||||
}
|
||||
|
||||
outCategory.Subcategories = append(outCategory.Subcategories, outSubCategory)
|
||||
outCategory.Subcategories = append(outCategory.Subcategories, outSubcategory)
|
||||
}
|
||||
sort.Slice(outCategory.Subcategories, func(i, j int) bool {
|
||||
return outCategory.Subcategories[i].ID < outCategory.Subcategories[j].ID
|
||||
})
|
||||
|
||||
out.Categories = append(out.Categories, outCategory)
|
||||
}
|
||||
sort.Slice(out.Categories, func(i, j int) bool {
|
||||
return out.Categories[i].ID < out.Categories[j].ID
|
||||
})
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
params, err := parseListCoursesParams(r)
|
||||
var span trace.Span
|
||||
ctx, span = webtracer.Start(ctx, "list")
|
||||
defer func() {
|
||||
span.End()
|
||||
}()
|
||||
|
||||
stats := bootstrap.MakeNewStats(10_240, 2_560_000, 1800)
|
||||
|
||||
pathParams, err := parseListCoursesParams(r)
|
||||
if handleError(ctx, err, w, c.log, "unable to parse list courses params") {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||
CourseThematic: params.courseThematic,
|
||||
LearningType: params.learningType,
|
||||
Limit: params.perPage,
|
||||
NextPageToken: params.nextPageToken,
|
||||
jsonParams, _ := json.Marshal(pathParams)
|
||||
span.SetAttributes(paramsAttr.String(string(jsonParams)))
|
||||
|
||||
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||
CourseThematic: pathParams.CourseThematic,
|
||||
LearningType: pathParams.LearningType,
|
||||
Limit: pathParams.PerPage,
|
||||
NextPageToken: pathParams.NextPageToken,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
courses := result.Courses
|
||||
templateCourses := mapDomainCourseToTemplate(courses...)
|
||||
templateCourses.NextPageToken = result.NextPageToken
|
||||
params := makeTemplListCoursesParams(listCoursesResult.AvailableCoursesOfSub, listCoursesResult.Courses...)
|
||||
|
||||
learningTypeList, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||
return
|
||||
}
|
||||
|
||||
templateCourses.AvailableLearningTypes = xslices.Map(learningTypeList.LearningTypes, func(in query.LearningType) IDNamePair {
|
||||
if in.ID == params.learningType {
|
||||
templateCourses.LearningTypeName = in.Name
|
||||
}
|
||||
return IDNamePair{
|
||||
params.FilterForm.AvailableLearningTypes = xslices.Map(learningTypeResult.LearningTypes, func(in query.LearningType) bootstrap.Category {
|
||||
outcategory := bootstrap.Category{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
IsActive: in.ID == params.learningType,
|
||||
}
|
||||
if in.ID == pathParams.LearningType {
|
||||
params.FilterForm.ActiveLearningType = outcategory
|
||||
}
|
||||
|
||||
return outcategory
|
||||
})
|
||||
|
||||
templateCourses.ActiveLearningType = params.learningType
|
||||
templateCourses.ActiveCourseThematic = params.courseThematic
|
||||
|
||||
if params.learningType != "" {
|
||||
if pathParams.LearningType != "" {
|
||||
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
|
||||
LearningTypeID: params.learningType,
|
||||
LearningTypeID: pathParams.LearningType,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
|
||||
return
|
||||
}
|
||||
|
||||
templateCourses.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) IDNamePair {
|
||||
if in.ID == params.courseThematic {
|
||||
templateCourses.CourseThematicName = in.Name
|
||||
}
|
||||
return IDNamePair{
|
||||
params.FilterForm.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) bootstrap.Category {
|
||||
outcategory := bootstrap.Category{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
IsActive: in.ID == params.courseThematic,
|
||||
}
|
||||
if pathParams.CourseThematic == in.ID {
|
||||
params.FilterForm.BreadcrumbsParams.ActiveCourseThematic = outcategory
|
||||
}
|
||||
|
||||
return outcategory
|
||||
})
|
||||
}
|
||||
|
||||
var tmpl *template.Template
|
||||
if c.useTailwind {
|
||||
tmpl = getTemplateHTMLBySpecificFiles(ctx, c.log, "list.html")
|
||||
c.log.DebugContext(
|
||||
ctx, "using bootstrap",
|
||||
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
|
||||
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
|
||||
)
|
||||
|
||||
params = bootstrap.ListCoursesParams{
|
||||
FilterForm: bootstrap.FilterFormParams{
|
||||
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
|
||||
ActiveLearningType: params.FilterForm.ActiveLearningType,
|
||||
ActiveCourseThematic: params.FilterForm.ActiveCourseThematic,
|
||||
},
|
||||
AvailableLearningTypes: params.FilterForm.AvailableLearningTypes,
|
||||
AvailableCourseThematics: params.FilterForm.AvailableCourseThematics,
|
||||
},
|
||||
Categories: params.Categories,
|
||||
}
|
||||
|
||||
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.CategoryContainer) int {
|
||||
if lhs.Count > rhs.Count {
|
||||
return 1
|
||||
} else if lhs.Count < rhs.Count {
|
||||
return -1
|
||||
} else {
|
||||
tmpl = getCoreTemplate(ctx, c.log)
|
||||
return 0
|
||||
}
|
||||
err = tmpl.ExecuteTemplate(w, "courses", templateCourses)
|
||||
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||
})
|
||||
|
||||
span.AddEvent("starting to render")
|
||||
err = bootstrap.ListCourses(bootstrap.PageCourses, stats, params).Render(ctx, w)
|
||||
span.AddEvent("render finished")
|
||||
|
||||
if handleError(ctx, err, w, c.log, "unable to render list courses") {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c courseServer) Get(w http.ResponseWriter, r *http.Request) {
|
||||
func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
id := mux.Vars(r)["course_id"]
|
||||
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
||||
ID: id,
|
||||
var span trace.Span
|
||||
ctx, span = webtracer.Start(ctx, "index")
|
||||
defer func() {
|
||||
span.End()
|
||||
}()
|
||||
|
||||
stats := bootstrap.MakeNewStats(1, 2, 3)
|
||||
|
||||
coursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{})
|
||||
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
params := bootstrap.MainPageParams{
|
||||
Categories: []bootstrap.IndexCourseCategoryItem{},
|
||||
}
|
||||
|
||||
coursesByLearningType := make(map[IDNamePair][]domain.Course)
|
||||
xslices.ForEach(coursesResult.Courses, func(in domain.Course) {
|
||||
pair := IDNamePair{
|
||||
ID: in.LearningTypeID,
|
||||
Name: in.LearningType,
|
||||
}
|
||||
coursesByLearningType[pair] = append(coursesByLearningType[pair], in)
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to get course") {
|
||||
return
|
||||
|
||||
for learningTypeInfo, courses := range coursesByLearningType {
|
||||
category := bootstrap.IndexCourseCategoryItem{
|
||||
ID: learningTypeInfo.ID,
|
||||
Name: learningTypeInfo.Name,
|
||||
Count: len(courses),
|
||||
}
|
||||
|
||||
payload, err := json.MarshalIndent(course, "", " ")
|
||||
if handleError(ctx, err, w, c.log, "unable to marshal json") {
|
||||
return
|
||||
}
|
||||
w.Header().Set("content-type", "application/json")
|
||||
w.Header().Set("content-length", strconv.Itoa(len(payload)))
|
||||
|
||||
_, err = w.Write([]byte(payload))
|
||||
if err != nil {
|
||||
xcontext.LogWithWarnError(ctx, c.log, err, "unable to write a message")
|
||||
}
|
||||
xslices.Shuffle(courses)
|
||||
if len(courses) > 3 {
|
||||
courses = courses[:3]
|
||||
}
|
||||
|
||||
func (c courseServer) GetShort(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
id := mux.Vars(r)["course_id"]
|
||||
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
||||
ID: id,
|
||||
names := xslices.Map(courses, func(in domain.Course) string {
|
||||
return in.Name
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to get course") {
|
||||
return
|
||||
|
||||
namesStr := strings.Join(names, ",")
|
||||
|
||||
category.Description = fmt.Sprintf(
|
||||
"Here you can find courses"+
|
||||
" such as %s",
|
||||
namesStr,
|
||||
)
|
||||
|
||||
params.Categories = append(params.Categories, category)
|
||||
}
|
||||
|
||||
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "course_info", course)
|
||||
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
return 0
|
||||
})
|
||||
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") {
|
||||
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) 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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 }}
|
||||
@ -1,14 +0,0 @@
|
||||
{{ define "htmlbody" }}
|
||||
{{ template "header" .}}
|
||||
{{ template "body" .}}
|
||||
{{ template "footer" .}}
|
||||
{{ end }}
|
||||
|
||||
{{ define "header" }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "body" }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "footer" }}
|
||||
{{ end }}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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 }}
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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}}
|
||||
Reference in New Issue
Block a user