Compare commits

..

19 Commits

Author SHA1 Message Date
348c737163 add host support 2024-09-25 00:48:47 +03:00
c3d6510a7d support metrics sending via grpc 2024-09-25 00:47:12 +03:00
c7fada2c54 update trace logic 2024-09-24 21:59:33 +03:00
c0f45d98c2 count courses by school 2024-06-16 23:55:49 +03:00
1a31006b21 Make filters by all options at once
Before script has been separated on two groups: for learning type and
course category and for school and sorting. It means when school is
choosen, learning type and course category resets on update.

TODO: need to add pagination
2024-05-02 23:24:41 +03:00
e8ac96a065 minor fix 2024-04-19 00:31:26 +03:00
3a9e01a683 rework list courses page to be flatten 2024-04-19 00:13:03 +03:00
035e9c848f minor web-view improvment 2024-04-11 10:33:14 +03:00
605e117586 add pagination 2024-04-07 23:49:06 +03:00
68810d93a7 add opentelemetry tracing 2024-04-02 15:23:22 +03:00
e7c2832865 Add command and query for organizations
* Added command and query for organizations
* Saving unknown organizations into database in `background` service
* Added `List` method in `OrganizationRepository`
2024-03-24 22:59:32 +03:00
9d2efcc1c4 implement organization repo 2024-03-24 16:51:39 +03:00
88a3cae4fa learning category repo 2024-03-16 17:44:43 +03:00
938d3cd307 add sqlite support 2024-03-12 14:44:22 +03:00
97986063df bootstrap migrate 2024-03-04 00:03:08 +03:00
251ecd94d4 minor fixes 2024-03-03 09:34:17 +03:00
6d1769ff24 make bootstrap site be more cool 2024-03-03 00:55:43 +03:00
af4a4f7840 bootstrap migrate 2024-03-02 19:34:33 +03:00
e5dfccabbf use tailwind 2024-02-10 20:22:30 +03:00
88 changed files with 8165 additions and 2753 deletions

1
.gitattributes vendored
View File

@ -1 +1,2 @@
./assets/kurious binary ./assets/kurious binary
*/**/*_templ.go binary

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
*.json *.json
bin bin
./tags ./tags
*.sqlite
.zed
*.log

View File

@ -1,3 +1,2 @@
with-expecter: true with-expecter: true
keeptree: True keeptree: True

View File

@ -1 +1 @@
cf55887b91f81f789d59205c41f8368 d65622032d35cb78ee1539f4ab4d875b

View File

@ -1,4 +1,4 @@
version: '3' version: "3"
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
@ -17,21 +17,34 @@ vars:
tasks: tasks:
install_tools: install_tools:
cmds: cmds:
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 - "[[ ! -f $GOBIN/golangci-lint ]] && go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 || echo golang-ci lint installed"
- go install github.com/a-h/templ/cmd/templ@v0.2.513 - "rm -rf $GOBIN/templ && go install github.com/a-h/templ/cmd/templ@v0.2.707 || echo templ installed"
- "[[ ! -f $GOBIN/mockery ]] && go install github.com/vektra/mockery/v2@v2.42.1 || echo mockery installed"
generate: generate:
run: once
cmds: cmds:
- "$GOBIN/templ generate" - "$GOBIN/templ generate"
sources: sources:
- "internal/kurious/ports/http/templ/*.templ" - "internal/kurious/ports/http/templ/*.templ"
- "internal/kurious/ports/http/bootstrap/*.templ"
generates: generates:
- "internal/kurious/ports/http/templ/*.go" - "internal/kurious/ports/http/templ/*.go"
- "internal/kurious/ports/http/bootstrap/*.go"
deps:
- install_tools
mocks:
run: once
cmd: "go generate ./internal/..."
deps:
- install_tools
check: check:
run: once
cmds: cmds:
- "$GOBIN/golangci-lint run ./..." - "$GOBIN/golangci-lint run ./..."
deps: deps:
- generate - generate
test: test:
run: once
cmds: cmds:
- go test ./internal/... - go test ./internal/...
deps: deps:
@ -52,8 +65,9 @@ tasks:
cmds: cmds:
- task: build_dev_cli - task: build_dev_cli
- task: build_background - task: build_background
- task: build_web
run: run:
deps: [build] deps: [build]
cmds: cmds:
- $GOBIN/sravnicli - $GOBIN/kuriousweb

View File

@ -8,9 +8,19 @@ import (
"git.loyso.art/frx/kurious/internal/common/config" "git.loyso.art/frx/kurious/internal/common/config"
) )
type dbEngine string
const (
DBEngineUnknown dbEngine = ""
DBEngineYDB dbEngine = "ydb"
DBEngineSqlite dbEngine = "sqlite"
)
type Config struct { type Config struct {
Log config.Log `json:"log"` Log config.Log `json:"log"`
YDB config.YDB `json:"ydb"` YDB config.YDB `json:"ydb"`
Sqlite config.Sqlite `json:"sqlite"`
DBEngine dbEngine `json:"db_engine"`
SyncSravniCron string `json:"sync_sravni_cron"` SyncSravniCron string `json:"sync_sravni_cron"`
DebugHTTP bool `json:"debug_http"` DebugHTTP bool `json:"debug_http"`
@ -37,5 +47,7 @@ func defaultConfig() Config {
Level: config.LogLevelInfo, Level: config.LogLevelInfo,
Format: config.LogFormatText, Format: config.LogFormatText,
}, },
// TODO: change to sqlite once it proven to be working
DBEngine: DBEngineYDB,
} }
} }

View File

@ -29,7 +29,13 @@ func main() {
} }
} }
const savingOrganizationIDInternalInsteadOfExternal = false
func app(ctx context.Context) error { func app(ctx context.Context) error {
if !savingOrganizationIDInternalInsteadOfExternal {
panic("fix saving ogranization id as external id instead of internal")
}
var cfgpath string var cfgpath string
if len(os.Args) > 1 { if len(os.Args) > 1 {
cfgpath = os.Args[1] cfgpath = os.Args[1]
@ -69,9 +75,19 @@ func app(ctx context.Context) error {
mapper := adapters.NewMemoryMapper(courseThematcisMapped, learningTypeMapped) mapper := adapters.NewMemoryMapper(courseThematcisMapped, learningTypeMapped)
var dbEngine service.RepositoryEngine
switch cfg.DBEngine {
case DBEngineSqlite:
dbEngine = service.RepositoryEngineSqlite
case DBEngineYDB:
dbEngine = service.RepositoryEngineYDB
}
app, err := service.NewApplication(ctx, service.ApplicationConfig{ app, err := service.NewApplication(ctx, service.ApplicationConfig{
LogConfig: cfg.Log, LogConfig: cfg.Log,
YDB: cfg.YDB, YDB: cfg.YDB,
Sqlite: cfg.Sqlite,
Engine: dbEngine,
}, mapper) }, mapper)
if err != nil { if err != nil {
return fmt.Errorf("making new application: %w", err) return fmt.Errorf("making new application: %w", err)

View File

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

View File

@ -61,6 +61,7 @@ func setupCLI(ctx context.Context) cli.App {
case "courses": case "courses":
out = state.Props.InitialReduxState.Dictionaries.Data.CourseThematics out = state.Props.InitialReduxState.Dictionaries.Data.CourseThematics
} }
log.InfoContext(ctx, "loaded state", slog.Any("state", out)) log.InfoContext(ctx, "loaded state", slog.Any("state", out))
return 0 return 0

View File

@ -44,7 +44,6 @@ func setupAPICommand(ctx context.Context) cli.Command {
WithOption(learningSelectionOpt). WithOption(learningSelectionOpt).
WithAction(newProductsFilterCountAction(ctx)) WithAction(newProductsFilterCountAction(ctx))
}) })
apiEducation := cli.NewCommand("education", "Education related category"). apiEducation := cli.NewCommand("education", "Education related category").
WithCommand(apiEducationListProducts). WithCommand(apiEducationListProducts).
WithCommand(apiEducationFilterCount) WithCommand(apiEducationFilterCount)
@ -137,10 +136,13 @@ func (a *listProductsAction) parse(args []string, options map[string]string) err
func (a *listProductsAction) handle() error { func (a *listProductsAction) handle() error {
params := sravni.ListEducationProductsParams{ params := sravni.ListEducationProductsParams{
LearningType: a.params.learningType, LearningType: a.params.learningType,
CoursesThematics: []string{a.params.courseThematic},
Limit: a.params.limit, Limit: a.params.limit,
Offset: a.params.offset, Offset: a.params.offset,
} }
if a.params.courseThematic != "" {
params.CoursesThematics = append(params.CoursesThematics, a.params.courseThematic)
}
result, err := a.client.ListEducationalProducts(a.ctx, params) result, err := a.client.ListEducationalProducts(a.ctx, params)
if err != nil { if err != nil {
return fmt.Errorf("listing education products: %w", err) return fmt.Errorf("listing education products: %w", err)

View File

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

View File

@ -11,7 +11,14 @@ import (
"git.loyso.art/frx/kurious/internal/common/generator" "git.loyso.art/frx/kurious/internal/common/generator"
"git.loyso.art/frx/kurious/internal/common/xcontext" "git.loyso.art/frx/kurious/internal/common/xcontext"
xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http" xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http"
"git.loyso.art/frx/kurious/pkg/xdefault"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
) )
func makePathTemplate(params ...string) string { func makePathTemplate(params ...string) string {
@ -28,50 +35,33 @@ func makePathTemplate(params ...string) string {
return sb.String() return sb.String()
} }
func setupHTTPWithTempl(srv xhttp.Server, router *mux.Router, log *slog.Logger) { func setupCoursesHTTP(srv xhttp.Server, router *mux.Router, _ *slog.Logger) {
coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true)
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() coursesAPI := srv.Courses()
coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true) router.Handle("/", http.RedirectHandler("/courses", http.StatusPermanentRedirect))
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() coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true)
courseRouter.HandleFunc("/", coursesAPI.Get).Methods(http.MethodGet)
courseRouter.HandleFunc("/short", coursesAPI.GetShort).Methods(http.MethodGet) coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
courseRouter.HandleFunc("/editdesc", coursesAPI.RenderEditDescription).Methods(http.MethodGet) coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
courseRouter.HandleFunc("/description", coursesAPI.UpdateCourseDescription).Methods(http.MethodPut)
muxHandleFunc(coursesRouter, "index", "/", coursesAPI.Index).Methods(http.MethodGet)
muxHandleFunc(coursesRouter, "list_learning", coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
muxHandleFunc(coursesRouter, "list_full", coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
} }
func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server { func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server {
router := mux.NewRouter() router := mux.NewRouter()
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { router.Use(
w.WriteHeader(http.StatusOK) middlewareCustomWriterInjector(),
}) mux.CORSMethodMiddleware(router),
middlewareLogger(log),
middlewareTrace(),
middlewareMetrics(),
)
router.Use(mux.CORSMethodMiddleware(router)) setupCoursesHTTP(srv, router, log)
router.Use(middlewareLogger(log, cfg.Engine))
if cfg.Engine == "templ" {
setupHTTPWithTempl(srv, router, log)
} else {
setupHTTPWithGoTemplates(srv, router, log)
}
if cfg.MountLive { if cfg.MountLive {
fs := http.FileServer(http.Dir("./assets/kurious/static/")) fs := http.FileServer(http.Dir("./assets/kurious/static/"))
@ -102,7 +92,7 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server
} }
} else { } else {
fs := kurious.AsHTTPFileHandler() fs := kurious.AsHTTPFileHandler()
router.PathPrefix("/").Handler(fs).Methods(http.MethodGet) router.PathPrefix("/*").Handler(fs).Methods(http.MethodGet)
} }
return &http.Server{ return &http.Server{
@ -111,7 +101,112 @@ 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)
})
}
}
type attributeStringKey string
func (k attributeStringKey) Value(value string) attribute.KeyValue {
return attribute.String(string(k), value)
}
func must[T any](value T, err error) T {
if err != nil {
panic(err.Error())
}
return value
}
func middlewareMetrics() mux.MiddlewareFunc {
requestDuration := must(webmetric.Float64Histogram(
semconv.HTTPServerRequestDurationName,
metric.WithUnit(semconv.HTTPServerRequestDurationUnit),
metric.WithDescription(semconv.HTTPServerRequestDurationDescription),
))
f := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
route := mux.CurrentRoute(r)
hpath, _ := route.GetPathTemplate()
attributes := make([]attribute.KeyValue, 0, 8)
attributes = append(
attributes,
semconv.HTTPRequestMethodOriginal(r.Method),
semconv.URLFull(hpath),
semconv.URLPath(r.URL.Path),
)
next.ServeHTTP(w, r)
if wr, ok := w.(*customResponseWriter); ok {
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
attributes = append(attributes,
semconv.HTTPResponseStatusCode(statusCode),
semconv.HTTPResponseBodySize(wr.wroteBytes),
)
}
elapsed := time.Since(start).Truncate(time.Millisecond)
requestDuration.Record(
r.Context(), elapsed.Seconds(), metric.WithAttributes(attributes...))
})
}
return f
}
func middlewareTrace() mux.MiddlewareFunc {
reqidAttr := attributeStringKey("http.request_id")
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
route := mux.CurrentRoute(r)
hpath, _ := route.GetPathTemplate()
ctx, span = webtracer.Start(
ctx, r.Method+" "+hpath,
trace.WithAttributes(
reqidAttr.Value(reqid),
semconv.HTTPRequestMethodOriginal(r.Method),
semconv.URLFull(hpath),
semconv.URLPath(r.URL.Path),
semconv.URLQuery(r.URL.RawQuery),
semconv.UserAgentOriginal(r.UserAgent()),
),
trace.WithSpanKind(trace.SpanKindServer),
)
defer span.End()
next.ServeHTTP(w, r.WithContext(ctx))
if wr, ok := w.(*customResponseWriter); ok {
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
span.SetAttributes(
semconv.HTTPResponseStatusCode(statusCode),
semconv.HTTPResponseBodySize(wr.wroteBytes),
)
if statusCode > 399 {
span.SetStatus(codes.Error, "error during request")
} else {
span.SetStatus(codes.Ok, "request completed")
}
}
})
}
}
func middlewareLogger(log *slog.Logger) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -119,11 +214,12 @@ func middlewareLogger(log *slog.Logger, engine string) mux.MiddlewareFunc {
if requestID == "" { if requestID == "" {
requestID = generator.RandomInt64ID() requestID = generator.RandomInt64ID()
} }
ctx = xcontext.WithLogFields( ctx = xcontext.WithLogFields(
ctx, ctx,
slog.String("request_id", requestID), slog.String("request_id", requestID),
slog.String("engine", engine),
) )
ctx = xcontext.WithRequestID(ctx, requestID)
xcontext.LogInfo( xcontext.LogInfo(
ctx, log, "incoming request", ctx, log, "incoming request",
@ -133,12 +229,45 @@ func middlewareLogger(log *slog.Logger, engine string) mux.MiddlewareFunc {
start := time.Now() start := time.Now()
next.ServeHTTP(w, r.WithContext(ctx)) 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( logfields := make([]slog.Attr, 0, 3)
ctx, log, "request processed", logfields = append(logfields, elapsed)
slog.Duration("elapsed", elapsed), if wr, ok := w.(*customResponseWriter); ok {
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
logfields = append(
logfields,
slog.Int("status_code", statusCode),
slog.Int("bytes_wrote", wr.wroteBytes),
) )
}
xcontext.LogInfo(ctx, log, "request processed", logfields...)
}) })
} }
} }
func wrapWithCustomWriter(origin http.ResponseWriter) *customResponseWriter {
return &customResponseWriter{
ResponseWriter: origin,
}
}
type customResponseWriter struct {
http.ResponseWriter
statusCode int
wroteBytes int
}
func (w *customResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *customResponseWriter) Write(data []byte) (n int, err error) {
n, err = w.ResponseWriter.Write(data)
w.wroteBytes += n
return n, err
}

View File

@ -46,6 +46,17 @@ func app(ctx context.Context) error {
log := config.NewSLogger(cfg.Log) 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) sravniClient, err := sravni.NewClient(ctx, log, false)
if err != nil { if err != nil {
return fmt.Errorf("unable to make new sravni client: %w", err) 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) 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{ app, err := service.NewApplication(ctx, service.ApplicationConfig{
LogConfig: cfg.Log, LogConfig: cfg.Log,
YDB: cfg.YDB, YDB: cfg.YDB,
Sqlite: cfg.Sqlite,
Engine: dbengine,
}, mapper) }, mapper)
if err != nil { if err != nil {
return fmt.Errorf("making new application: %w", err) 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") xcontext.LogInfo(ctx, log, "server closed successfuly")
err = shutdownOtel(sdctx)
if err != nil {
return fmt.Errorf("shutting down sdk: %w", err)
}
return nil return nil
}) })

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

@ -0,0 +1,243 @@
package main
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"git.loyso.art/frx/kurious/internal/common/config"
"google.golang.org/grpc/encoding/gzip"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
var (
webtracer = otel.Tracer("kuriweb.http")
webmetric = otel.Meter("kuriweb.http")
)
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
}
resource, err := makeServiceResource(ctx)
if err != nil {
return shutdown, fmt.Errorf("making service resource: %w", err)
}
prop := newPropagator()
otel.SetTextMapPropagator(prop)
tracerProvider, err := newCommonTraceProvider(ctx, TraceProviderParams{
Endpoint: cfg.Endpoint,
Type: cfg.Type,
AuthHeader: cfg.APIHeader,
APIKey: cfg.APIKey,
}, resource)
if err != nil {
return nil, handleError(err)
}
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
if cfg.ShowMetrics {
meterProvider, err := newCommonMeterProvider(ctx, meterProviderParams{
Endpoint: cfg.Endpoint,
Type: cfg.Type,
AuthHeaderKey: cfg.APIHeader,
AuthHeaderValue: cfg.APIKey,
ReadInterval: time.Second * 15,
}, resource)
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{},
)
}
type TraceProviderParams struct {
Endpoint string
APIKey string
AuthHeader string
Type config.TraceClientType
}
func makeServiceResource(ctx context.Context) (*resource.Resource, error) {
r, err := resource.New(
ctx,
resource.WithDetectors(
resource.StringDetector(semconv.SchemaURL, semconv.ServiceNameKey, func() (string, error) {
return "bigstats:kuriweb", nil
}),
),
resource.WithHost(),
resource.WithAttributes(
semconv.ServiceName("bigstats:kuriweb"),
semconv.DeploymentEnvironment("production"),
),
)
if err != nil {
return nil, fmt.Errorf("making new resource: %w", err)
}
return resource.Merge(resource.Default(), r)
}
func newCommonTraceProvider(ctx context.Context, params TraceProviderParams, r *resource.Resource) (tp *trace.TracerProvider, err error) {
opts := make([]trace.TracerProviderOption, 0, 4)
opts = append(
opts,
trace.WithSampler(trace.AlwaysSample()),
trace.WithResource(r),
)
if params.Type != config.TraceClientTypeUnset {
var spanExporter trace.SpanExporter
var headers map[string]string
if params.AuthHeader != "" {
headers = make(map[string]string, 1)
headers[params.AuthHeader] = params.APIKey
}
switch params.Type {
case config.TraceClientTypeGRPC:
spanExporter, err = otlptracegrpc.New(
ctx,
otlptracegrpc.WithEndpointURL(params.Endpoint),
otlptracegrpc.WithInsecure(),
otlptracegrpc.WithHeaders(headers),
otlptracegrpc.WithCompressor(gzip.Name),
)
case config.TraceClientTypeHTTP:
httpClient := otlptracehttp.NewClient(
otlptracehttp.WithEndpointURL(params.Endpoint),
otlptracehttp.WithHeaders(headers),
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
)
spanExporter, err = otlptrace.New(
ctx, httpClient,
)
case config.TraceClientTypeStdout:
spanExporter, err = stdouttrace.New(stdouttrace.WithPrettyPrint())
default:
return nil, fmt.Errorf("unsupported provider type")
}
if err != nil {
return nil, fmt.Errorf("making trace exporter: %w", err)
}
opts = append(opts, trace.WithBatcher(spanExporter))
}
tp = trace.NewTracerProvider(opts...)
return tp, nil
}
type meterProviderParams struct {
Endpoint string
AuthHeaderKey string
AuthHeaderValue string
ReadInterval time.Duration
Type config.TraceClientType
}
func newCommonMeterProvider(ctx context.Context, params meterProviderParams, r *resource.Resource) (*metric.MeterProvider, error) {
var exporter metric.Exporter
var err error
switch params.Type {
case config.TraceClientTypeGRPC:
headers := make(map[string]string, 1)
if params.AuthHeaderKey != "" {
headers[params.AuthHeaderKey] = params.AuthHeaderValue
}
exporter, err = otlpmetricgrpc.New(
ctx,
otlpmetricgrpc.WithEndpointURL(params.Endpoint),
otlpmetricgrpc.WithHeaders(headers),
otlpmetricgrpc.WithCompressor(gzip.Name),
otlpmetricgrpc.WithTemporalitySelector(preferDeltaTemporalitySelector),
otlpmetricgrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("making grpc exporter: %w", err)
}
case config.TraceClientTypeStdout:
exporter, err = stdoutmetric.New()
if err != nil {
return nil, fmt.Errorf("making stdout exporter: %w", err)
}
default:
return nil, nil
}
reader := metric.NewPeriodicReader(
exporter,
metric.WithInterval(params.ReadInterval),
)
provider := metric.NewMeterProvider(
metric.WithReader(reader),
metric.WithResource(r),
)
return provider, nil
}
func muxHandleFunc(router *mux.Router, name, path string, hf http.HandlerFunc) *mux.Route {
// h := otelhttp.WithRouteTag(path, hf)
return router.Handle(path, hf).Name(name)
}
func preferDeltaTemporalitySelector(kind metric.InstrumentKind) metricdata.Temporality {
switch kind {
case metric.InstrumentKindCounter,
metric.InstrumentKindObservableCounter,
metric.InstrumentKindHistogram:
return metricdata.DeltaTemporality
default:
return metricdata.CumulativeTemporality
}
}

69
go.mod
View File

@ -1,34 +1,65 @@
module git.loyso.art/frx/kurious module git.loyso.art/frx/kurious
go 1.21 go 1.22
require ( require (
github.com/go-resty/resty/v2 v2.10.0 github.com/a-h/templ v0.2.707
github.com/go-resty/resty/v2 v2.13.1
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/robfig/cron/v3 v3.0.0 github.com/jmoiron/sqlx v1.4.0
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.9.0
github.com/teris-io/cli v1.0.1 github.com/teris-io/cli v1.0.1
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 github.com/ydb-platform/ydb-go-sdk/v3 v3.74.3
github.com/ydb-platform/ydb-go-yc v0.12.1 github.com/ydb-platform/ydb-go-yc v0.12.1
golang.org/x/net v0.18.0 go.opentelemetry.io/otel v1.30.0
golang.org/x/sync v0.5.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0
go.opentelemetry.io/otel/metric v1.30.0
go.opentelemetry.io/otel/sdk v1.30.0
go.opentelemetry.io/otel/sdk/metric v1.30.0
go.opentelemetry.io/otel/trace v1.30.0
golang.org/x/net v0.29.0
golang.org/x/sync v0.8.0
golang.org/x/time v0.5.0 golang.org/x/time v0.5.0
google.golang.org/grpc v1.66.1
modernc.org/sqlite v1.30.1
) )
require ( require (
github.com/a-h/templ v0.2.513 // indirect 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/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20240529120826-df2b24336f42 // indirect
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7 // indirect
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect
golang.org/x/sys v0.14.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/sys v0.25.0 // indirect
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect golang.org/x/text v0.18.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
modernc.org/libc v1.53.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
) )

183
go.sum
View File

@ -509,14 +509,16 @@ cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vf
cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA=
cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/a-h/templ v0.2.513 h1:ZmwGAOx4NYllnHy+FTpusc4+c5msoMpPIYX0Oy3dNqw= github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
github.com/a-h/templ v0.2.513/go.mod h1:9gZxTLtRzM3gQxO8jr09Na0v8/jfliS97S9W5SScanM= github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
@ -527,6 +529,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/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.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1/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.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.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
@ -556,6 +560,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -584,10 +590,17 @@ 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-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-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= 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.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/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.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-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= github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@ -608,7 +621,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -627,7 +639,6 @@ 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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@ -648,8 +659,9 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -670,11 +682,13 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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.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.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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.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.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
@ -697,11 +711,17 @@ 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 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.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.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 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 v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
@ -718,17 +738,28 @@ 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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 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/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.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
@ -744,13 +775,19 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/rekby/fixenv v0.3.2/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c= github.com/rekby/fixenv v0.3.2/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=
github.com/rekby/fixenv v0.6.1 h1:jUFiSPpajT4WY2cYuc++7Y1zWrnCxnovGCIX72PZniM=
github.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 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.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.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= 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/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= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@ -760,6 +797,8 @@ github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcD
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@ -769,21 +808,21 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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 h1:J6jnVHC552uqx7zT+Ux0++tIvLmJQULqxVhCid2u/Gk=
github.com/teris-io/cli v1.0.1/go.mod h1:V9nVD5aZ873RU/tQXLSXO8FieVPQhQvuNohsdsKXsGw= github.com/teris-io/cli v1.0.1/go.mod h1:V9nVD5aZ873RU/tQXLSXO8FieVPQhQvuNohsdsKXsGw=
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-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-20240529120826-df2b24336f42 h1:l5Wu1kRcM34HqBR2FZI6tWc6QKyPziNj5fGZ4eXTCRI=
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-genproto v0.0.0-20240529120826-df2b24336f42/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20221215182650-986f9d10542f/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= github.com/ydb-platform/ydb-go-genproto v0.0.0-20221215182650-986f9d10542f/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20230528143953-42c825ace222/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= github.com/ydb-platform/ydb-go-genproto v0.0.0-20230528143953-42c825ace222/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd h1:dzWP1Lu+A40W883dK/Mr3xyDSM/2MggS8GtHT0qgAnE= github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7 h1:nL8XwD6fSst7xFUirkaWJmE7kM0CdWRYgu6+YQer1d4=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
github.com/ydb-platform/ydb-go-sdk/v3 v3.44.0/go.mod h1:oSLwnuilwIpaF5bJJMAofnGgzPJusoI3zWMNb8I+GnM= github.com/ydb-platform/ydb-go-sdk/v3 v3.44.0/go.mod h1:oSLwnuilwIpaF5bJJMAofnGgzPJusoI3zWMNb8I+GnM=
github.com/ydb-platform/ydb-go-sdk/v3 v3.47.3/go.mod h1:bWnOIcUHd7+Sl7DN+yhyY1H/I61z53GczvwJgXMgvj0= github.com/ydb-platform/ydb-go-sdk/v3 v3.47.3/go.mod h1:bWnOIcUHd7+Sl7DN+yhyY1H/I61z53GczvwJgXMgvj0=
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 h1:E0yUuuX7UmPxXm92+yQCjMveLFO3zfvYFIJVuAqsVRA= github.com/ydb-platform/ydb-go-sdk/v3 v3.74.3 h1:QHjqt1GA/f/O6kF5MNjmnSRG/sK1fmQivIFs1PeUgeo=
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2/go.mod h1:fjBLQ2TdQNl4bMjuWl9adoTGBypwUTPoGC+EqYqiIcU= github.com/ydb-platform/ydb-go-sdk/v3 v3.74.3/go.mod h1:ZXl2InwGFiZKojWCsj6tW/SGFbch7zaehQWX5CjXMEI=
github.com/ydb-platform/ydb-go-yc v0.12.1 h1:qw3Fa+T81+Kpu5Io2vYHJOwcrYrVjgJlT6t/0dOXJrA= github.com/ydb-platform/ydb-go-yc v0.12.1 h1:qw3Fa+T81+Kpu5Io2vYHJOwcrYrVjgJlT6t/0dOXJrA=
github.com/ydb-platform/ydb-go-yc v0.12.1/go.mod h1:t/ZA4ECdgPWjAb4jyDe8AzQZB5dhpGbi3iCahFaNwBY= github.com/ydb-platform/ydb-go-yc v0.12.1/go.mod h1:t/ZA4ECdgPWjAb4jyDe8AzQZB5dhpGbi3iCahFaNwBY=
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 h1:9E5q8Nsy2RiJMZDNVy0A3KUrIMBPakJ2VgloeWbcI84= github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 h1:9E5q8Nsy2RiJMZDNVy0A3KUrIMBPakJ2VgloeWbcI84=
@ -805,9 +844,37 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0 h1:WypxHH02KX2poqqbaadmkMYalGyy/vil4HE4PM4nRJc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0/go.mod h1:U79SV99vtvGSEBeeHnpgGJfTsnsdkWLpPN/CcHAzBSI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 h1:/jlt1Y8gXWiHG9FBx6cJaIC5hYx5Fe64nC8w5Cylt/0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0/go.mod h1:bmToOGOBZ4hA9ghphIc1PAf66VA8KOtsuy3+ScStG20=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 h1:/0YaXu3755A/cFbtXp+21lkXgI0QE5avTWA2HjU9/WE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0/go.mod h1:m7SFxp0/7IxmJPLIY3JhOcU9CoFzDaCPL6xxQIxhA+o=
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM=
go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y=
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -817,7 +884,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -875,6 +943,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -932,9 +1002,10 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -979,8 +1050,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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1058,9 +1129,10 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@ -1069,7 +1141,8 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1086,15 +1159,15 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1158,6 +1231,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1361,12 +1436,10 @@ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
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/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -1407,8 +1480,8 @@ google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsA
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@ -1426,10 +1499,11 @@ 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.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.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.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -1451,13 +1525,23 @@ lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v4 v4.21.3 h1:2mhBdWKtivdFlLR1ecKXTljPG1mfvbByX7QKztAIJl8=
modernc.org/cc/v4 v4.21.3/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
modernc.org/ccgo/v4 v4.18.1 h1:1zF5kPBFq/ZVTulBOKgQPQITdOzzyBUfC51gVYP62E4=
modernc.org/ccgo/v4 v4.18.1/go.mod h1:ao1fAxf9a2KEOL15WY8+yP3wnpaOpP/QuyFOZ9HJolM=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8=
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
@ -1466,19 +1550,34 @@ modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s=
modernc.org/libc v1.53.3 h1:9O0aSLZuHPgp49we24NoFFteRgXNLGBAQ3TODrW3XLg=
modernc.org/libc v1.53.3/go.mod h1:kb+Erju4FfHNE59xd2fNpv5CBeAeej6fHbx8p8xaiyI=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk=
modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

152
htmlexamples/core.html Normal file
View File

@ -0,0 +1,152 @@
<!doctype html>
<html lang="en">
<head>
<title>Test page</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
</head>
<body data-bs-theme="dark">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Kurious</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link" aria-current="page" href="/index.html">Home</a>
<a class="nav-link" href="/courses.html">Courses</a>
<a class="nav-link active" href="/core.html">About us</a>
</div>
</div>
</div>
</nav>
<div class="container">
<nav style="--bs-breadcrumb-divider: '>'" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item" aria-current="page">
<a href="#">Course</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Theme</li>
</ol>
</nav>
<div class="row row-cols-1 row-cols-md-4 g-4">
<div class="col">
<div class="card">
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
<div class="card-body">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<!-- <a href="#" class="btn btn-primary">Go somewhere</a> -->
</div>
<div class="list-group">
<a href="#" class="btn btn-primary">Buy for 399.99$</a>
<small class="text-body-secondary"></small>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary col">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
<div class="card-body">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
<div class="card-body">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
<div class="card-body">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
<div class="col">
<div class="card">
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
<div class="card-body">
<h5 class="card-title">
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
nobis itaque modi dignissimos ...
</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
<div class="card-footer text-end">
<small class="text-body-secondary">399.99$</small>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

277
htmlexamples/courses.html Normal file
View File

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

151
htmlexamples/index.html Normal file
View File

@ -0,0 +1,151 @@
<!doctype html>
<html lang="en">
<head>
<title>Test page</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body data-bs-theme="dark" style="margin: 0">
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
<div class="container-fluid">
<a class="navbar-brand" href="/index.html">Kurious</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse" id="navbarSupportedContent">
<ul class="navbar-nav mb-lg-0 mb-2 me-auto">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/index.html">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/courses.html">Courses</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/core.html">About us</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<div class="row upper mb-4 text-center" style="min-height: 4rem">
<p class="justify-content-center">Here you can find course for any taste</p>
</div>
<div class="container w-75 mb-4">
<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>
<ul>
<li><span class="d-inline-block text-truncate col-8">web-development</span></li>
<li>backend development</li>
<li>frontend development</li>
</ul>
<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>
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link">Previous</a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
</body>
</html>

View File

@ -31,7 +31,7 @@ type Client interface {
ListEducationalProducts( ListEducationalProducts(
ctx context.Context, ctx context.Context,
params ListEducationProductsParams, params ListEducationProductsParams,
) (result listEducationProductsResponse, err error) ) (result ListEducationProductsResponse, err error)
ListEducationalProductsFilterCount( ListEducationalProductsFilterCount(
ctx context.Context, ctx context.Context,
params ListEducationProductsParams, params ListEducationProductsParams,
@ -177,7 +177,7 @@ type listEducationProductsRequest struct {
SortDirection string `json:"sortDirection"` SortDirection string `json:"sortDirection"`
} }
type listEducationProductsResponse struct { type ListEducationProductsResponse struct {
Items []Course `json:"items"` Items []Course `json:"items"`
Organizations map[string]Organization `json:"organizations"` Organizations map[string]Organization `json:"organizations"`
@ -188,7 +188,7 @@ type listEducationProductsResponse struct {
func (c *client) ListEducationalProducts( func (c *client) ListEducationalProducts(
ctx context.Context, ctx context.Context,
params ListEducationProductsParams, params ListEducationProductsParams,
) (result listEducationProductsResponse, err error) { ) (result ListEducationProductsResponse, err error) {
const urlPath = "/v1/education/products" const urlPath = "/v1/education/products"
const defaultLimit = 1 const defaultLimit = 1
const defaultSortProp = "advertising.position" const defaultSortProp = "advertising.position"

View File

@ -0,0 +1,208 @@
// Code generated by mockery v2.42.1. DO NOT EDIT.
package mocks
import (
context "context"
sravni "git.loyso.art/frx/kurious/internal/common/client/sravni"
mock "github.com/stretchr/testify/mock"
)
// Client is an autogenerated mock type for the Client type
type Client struct {
mock.Mock
}
type Client_Expecter struct {
mock *mock.Mock
}
func (_m *Client) EXPECT() *Client_Expecter {
return &Client_Expecter{mock: &_m.Mock}
}
// GetMainPageState provides a mock function with given fields:
func (_m *Client) GetMainPageState() (*sravni.PageState, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetMainPageState")
}
var r0 *sravni.PageState
var r1 error
if rf, ok := ret.Get(0).(func() (*sravni.PageState, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *sravni.PageState); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*sravni.PageState)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_GetMainPageState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMainPageState'
type Client_GetMainPageState_Call struct {
*mock.Call
}
// GetMainPageState is a helper method to define mock.On call
func (_e *Client_Expecter) GetMainPageState() *Client_GetMainPageState_Call {
return &Client_GetMainPageState_Call{Call: _e.mock.On("GetMainPageState")}
}
func (_c *Client_GetMainPageState_Call) Run(run func()) *Client_GetMainPageState_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Client_GetMainPageState_Call) Return(_a0 *sravni.PageState, _a1 error) *Client_GetMainPageState_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Client_GetMainPageState_Call) RunAndReturn(run func() (*sravni.PageState, error)) *Client_GetMainPageState_Call {
_c.Call.Return(run)
return _c
}
// ListEducationalProducts provides a mock function with given fields: ctx, params
func (_m *Client) ListEducationalProducts(ctx context.Context, params sravni.ListEducationProductsParams) (sravni.ListEducationProductsResponse, error) {
ret := _m.Called(ctx, params)
if len(ret) == 0 {
panic("no return value specified for ListEducationalProducts")
}
var r0 sravni.ListEducationProductsResponse
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, sravni.ListEducationProductsParams) (sravni.ListEducationProductsResponse, error)); ok {
return rf(ctx, params)
}
if rf, ok := ret.Get(0).(func(context.Context, sravni.ListEducationProductsParams) sravni.ListEducationProductsResponse); ok {
r0 = rf(ctx, params)
} else {
r0 = ret.Get(0).(sravni.ListEducationProductsResponse)
}
if rf, ok := ret.Get(1).(func(context.Context, sravni.ListEducationProductsParams) error); ok {
r1 = rf(ctx, params)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_ListEducationalProducts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListEducationalProducts'
type Client_ListEducationalProducts_Call struct {
*mock.Call
}
// ListEducationalProducts is a helper method to define mock.On call
// - ctx context.Context
// - params sravni.ListEducationProductsParams
func (_e *Client_Expecter) ListEducationalProducts(ctx interface{}, params interface{}) *Client_ListEducationalProducts_Call {
return &Client_ListEducationalProducts_Call{Call: _e.mock.On("ListEducationalProducts", ctx, params)}
}
func (_c *Client_ListEducationalProducts_Call) Run(run func(ctx context.Context, params sravni.ListEducationProductsParams)) *Client_ListEducationalProducts_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(sravni.ListEducationProductsParams))
})
return _c
}
func (_c *Client_ListEducationalProducts_Call) Return(result sravni.ListEducationProductsResponse, err error) *Client_ListEducationalProducts_Call {
_c.Call.Return(result, err)
return _c
}
func (_c *Client_ListEducationalProducts_Call) RunAndReturn(run func(context.Context, sravni.ListEducationProductsParams) (sravni.ListEducationProductsResponse, error)) *Client_ListEducationalProducts_Call {
_c.Call.Return(run)
return _c
}
// ListEducationalProductsFilterCount provides a mock function with given fields: ctx, params
func (_m *Client) ListEducationalProductsFilterCount(ctx context.Context, params sravni.ListEducationProductsParams) (sravni.ProductsFilterCount, error) {
ret := _m.Called(ctx, params)
if len(ret) == 0 {
panic("no return value specified for ListEducationalProductsFilterCount")
}
var r0 sravni.ProductsFilterCount
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, sravni.ListEducationProductsParams) (sravni.ProductsFilterCount, error)); ok {
return rf(ctx, params)
}
if rf, ok := ret.Get(0).(func(context.Context, sravni.ListEducationProductsParams) sravni.ProductsFilterCount); ok {
r0 = rf(ctx, params)
} else {
r0 = ret.Get(0).(sravni.ProductsFilterCount)
}
if rf, ok := ret.Get(1).(func(context.Context, sravni.ListEducationProductsParams) error); ok {
r1 = rf(ctx, params)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_ListEducationalProductsFilterCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListEducationalProductsFilterCount'
type Client_ListEducationalProductsFilterCount_Call struct {
*mock.Call
}
// ListEducationalProductsFilterCount is a helper method to define mock.On call
// - ctx context.Context
// - params sravni.ListEducationProductsParams
func (_e *Client_Expecter) ListEducationalProductsFilterCount(ctx interface{}, params interface{}) *Client_ListEducationalProductsFilterCount_Call {
return &Client_ListEducationalProductsFilterCount_Call{Call: _e.mock.On("ListEducationalProductsFilterCount", ctx, params)}
}
func (_c *Client_ListEducationalProductsFilterCount_Call) Run(run func(ctx context.Context, params sravni.ListEducationProductsParams)) *Client_ListEducationalProductsFilterCount_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(sravni.ListEducationProductsParams))
})
return _c
}
func (_c *Client_ListEducationalProductsFilterCount_Call) Return(result sravni.ProductsFilterCount, err error) *Client_ListEducationalProductsFilterCount_Call {
_c.Call.Return(result, err)
return _c
}
func (_c *Client_ListEducationalProductsFilterCount_Call) RunAndReturn(run func(context.Context, sravni.ListEducationProductsParams) (sravni.ProductsFilterCount, error)) *Client_ListEducationalProductsFilterCount_Call {
_c.Call.Return(run)
return _c
}
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewClient(t interface {
mock.TestingT
Cleanup(func())
}) *Client {
mock := &Client{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -12,6 +12,6 @@ func (NoopClient) GetMainPageState() (*PageState, error) {
return nil, errors.ErrNotImplemented return nil, errors.ErrNotImplemented
} }
func (NoopClient) ListEducationalProducts(context.Context, ListEducationProductsParams) (listEducationProductsResponse, error) { func (NoopClient) ListEducationalProducts(context.Context, ListEducationProductsParams) (ListEducationProductsResponse, error) {
return listEducationProductsResponse{}, errors.ErrNotImplemented return ListEducationProductsResponse{}, errors.ErrNotImplemented
} }

View File

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

View File

@ -0,0 +1,8 @@
package config
import "time"
type Sqlite struct {
DSN string `json:"dsn"`
ShutdownTimeout time.Duration `json:"shutdown_timeout"`
}

View File

@ -0,0 +1,38 @@
package config
import "errors"
type TraceClientType uint8
const (
TraceClientTypeUnset TraceClientType = iota
TraceClientTypeHTTP
TraceClientTypeGRPC
TraceClientTypeStdout
)
func (t *TraceClientType) UnmarshalText(data []byte) error {
dataStr := string(data)
switch dataStr {
case "http":
*t = TraceClientTypeHTTP
case "grpc":
*t = TraceClientTypeGRPC
case "stdout":
*t = TraceClientTypeStdout
case "":
default:
return errors.New("unsupported value " + dataStr)
}
return nil
}
type Trace struct {
Endpoint string `json:"endpoint"`
APIKey string `json:"api_key"`
APIHeader string `json:"api_header"`
Type TraceClientType `json:"type"`
ShowMetrics bool `json:"show_metrics"`
}

View File

@ -2,11 +2,24 @@ package decorator
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"strings"
"time" "time"
"git.loyso.art/frx/kurious/internal/common/xcontext" "git.loyso.art/frx/kurious/internal/common/xcontext"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var (
nameAttribute = attribute.Key("cq.name")
argsAttribute = attribute.Key("cq.args")
apiTracer = otel.Tracer("api")
) )
type commandLoggingDecorator[T any] struct { type commandLoggingDecorator[T any] struct {
@ -18,6 +31,17 @@ func (c commandLoggingDecorator[T]) Handle(ctx context.Context, cmd T) (err erro
handlerName := getTypeName[T]() handlerName := getTypeName[T]()
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName)) 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, "command "+handlerName)
span.SetAttributes(
nameAttribute.String(handlerName),
argsAttribute.String(argsBuilder.String()),
)
xcontext.LogDebug(ctx, c.log, "executing command") xcontext.LogDebug(ctx, c.log, "executing command")
start := time.Now() 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) xcontext.LogInfo(ctx, c.log, "command executed successfuly", elapsed)
} else { } else {
xcontext.LogError(ctx, c.log, "command execution failed", elapsed, slog.Any("err", err)) xcontext.LogError(ctx, c.log, "command execution failed", elapsed, slog.Any("err", err))
span.RecordError(err)
} }
span.End()
}() }()
return c.base.Handle(ctx, cmd) 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) { func (q queryLoggingDecorator[Q, U]) Handle(ctx context.Context, query Q) (entity U, err error) {
handlerName := getTypeName[Q]() handlerName := getTypeName[Q]()
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName)) 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, "query "+handlerName)
span.SetAttributes(
nameAttribute.String(handlerName),
argsAttribute.String(argsBuilder.String()),
)
xcontext.LogDebug(ctx, q.log, "executing command") xcontext.LogDebug(ctx, q.log, "executing command")
start := time.Now() 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) xcontext.LogInfo(ctx, q.log, "command executed successfuly", elapsed)
} else { } else {
xcontext.LogError(ctx, q.log, "command execution failed", elapsed, slog.Any("err", err)) 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) return q.base.Handle(ctx, query)

View File

@ -6,6 +6,16 @@ import (
) )
type ctxLogKey struct{} 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 { type ctxLogAttrStore struct {
attrs []slog.Attr attrs []slog.Attr

View File

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

View File

@ -0,0 +1,103 @@
package xslices
import (
"sync"
)
func NewLRU[K comparable, T any](capacity int) *lru[K, T] {
return &lru[K, T]{
items: make(map[K]*lruNode[K, T], capacity),
capacity: capacity,
}
}
type lruNode[K comparable, T any] struct {
key K
value T
next *lruNode[K, T]
prev *lruNode[K, T]
}
type lru[K comparable, T any] struct {
items map[K]*lruNode[K, T]
length int
capacity int
first *lruNode[K, T]
last *lruNode[K, T]
mu sync.RWMutex
}
func (l *lru[K, T]) Push(key K, value T) {
l.mu.Lock()
defer l.mu.Unlock()
node, ok := l.items[key]
if ok {
l.bumpUnsafe(node)
return
}
node = &lruNode[K, T]{
key: key,
value: value,
next: l.first,
}
if l.first != nil {
l.first.prev = node
}
if l.last == nil {
l.last = node
}
l.first = node
l.items[key] = node
if l.length == l.capacity && l.last != nil {
deletedNode := l.last
delete(l.items, deletedNode.key)
l.last = l.last.prev
return
}
l.length++
}
func (l *lru[K, T]) Get(key K) (T, bool) {
l.mu.Lock()
defer l.mu.Unlock()
node, ok := l.items[key]
if !ok {
var t T
return t, false
}
out := node.value
l.bumpUnsafe(node)
return out, true
}
func (l *lru[K, T]) bumpUnsafe(node *lruNode[K, T]) {
if l.first == node {
return
}
if node.next != nil {
node.next.prev = node.prev
}
if node.prev != nil {
node.prev.next = node.next
}
node.next = l.first
l.first.prev = node
l.first = node
node.prev = nil
}

View File

@ -0,0 +1,23 @@
package xslices
import "testing"
func TestLRU(t *testing.T) {
lru := NewLRU[int, string](3)
for i := 0; i < 4; i++ {
lru.Push(i, "v")
}
for i := 0; i < 4; i++ {
_, found := lru.Get(i)
if i == 0 {
if found {
t.Error("expected value to be flushed out of cache")
}
continue
}
if !found {
t.Errorf("expected value %d to be found", i)
}
}
}

View File

@ -0,0 +1,40 @@
package adapters
import (
"strings"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
)
var dbTracer = otel.Tracer("adapters.db")
var (
dbSystemAttr = attribute.Key("db.system")
dbNameAttr = attribute.Key("db.name")
dbStatementAttr = attribute.Key("db.statement")
dbOperationAttr = attribute.Key("db.operation")
dbTableAttr = attribute.Key("db.sql.table")
dbArgumentsAttr = attribute.Key("db.arguments")
)
type domainer[T any] interface {
AsDomain() T
}
func asDomainFunc[T any, U domainer[T]](in U) (out T) {
return in.AsDomain()
}
func joinColumns(columns []string) string {
return strings.Join(columns, ",")
}
func namedArgColumns(columns []string) string {
out := make([]string, len(columns))
for i, col := range columns {
out[i] = ":" + col
}
return joinColumns(out)
}

View File

@ -1,21 +1,111 @@
package adapters package adapters
import (
"context"
"fmt"
"git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
type inMemoryMapper struct { type inMemoryMapper struct {
courseThematicsByID map[string]string courseThematicsByID map[string]string
learningTypeByID map[string]string learningTypeByID map[string]string
stats map[string]domain.LearningTypeStat
courseThematicByLearningType map[string]string
totalCount int
} }
func NewMemoryMapper(courseThematics, learningType map[string]string) inMemoryMapper { func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryMapper {
return inMemoryMapper{ return &inMemoryMapper{
courseThematicsByID: courseThematics, courseThematicsByID: courseThematics,
learningTypeByID: learningType, learningTypeByID: learningType,
} }
} }
func (m inMemoryMapper) CourseThematicNameByID(id string) string { func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepository) error {
const batchSize = 1000
m.stats = map[string]domain.LearningTypeStat{}
m.courseThematicByLearningType = map[string]string{}
var offset int
for {
result, err := cr.List(ctx, domain.ListCoursesParams{
LearningType: "",
CourseThematic: "",
Limit: batchSize + 1,
Offset: offset,
})
if err != nil {
return fmt.Errorf("listing courses: %w", err)
}
courses := result.Courses
if len(result.Courses) >= batchSize {
courses = result.Courses[:batchSize]
}
m.totalCount += len(courses)
xslices.ForEach(courses, func(course domain.Course) {
stat, ok := m.stats[course.LearningTypeID]
stat.Count++
if !ok {
stat.CourseThematic = map[string]int{}
}
stat.CourseThematic[course.ThematicID]++
m.stats[course.LearningTypeID] = stat
m.courseThematicByLearningType[course.ThematicID] = course.LearningTypeID
})
if len(result.Courses) != batchSize+1 {
break
}
offset += batchSize
}
return nil
}
func (m *inMemoryMapper) GetCounts(byCourseThematic, byLearningType string) int {
if byCourseThematic != "" && byLearningType == "" {
byLearningType = m.courseThematicByLearningType[byCourseThematic]
}
if byLearningType != "" {
stat := m.stats[byLearningType]
if byCourseThematic != "" {
return stat.CourseThematic[byCourseThematic]
}
return stat.Count
}
return m.totalCount
}
func (m *inMemoryMapper) GetStats(copyMap bool) map[string]domain.LearningTypeStat {
if !copyMap {
return m.stats
}
out := make(map[string]domain.LearningTypeStat, len(m.stats))
for learningType, stats := range m.stats {
copiedStats := make(map[string]int, len(stats.CourseThematic))
for courseThematic, count := range stats.CourseThematic {
copiedStats[courseThematic] = count
}
out[learningType] = domain.LearningTypeStat{
Count: stats.Count,
CourseThematic: copiedStats,
}
}
return out
}
func (m *inMemoryMapper) CourseThematicNameByID(id string) string {
return m.courseThematicsByID[id] return m.courseThematicsByID[id]
} }
func (m inMemoryMapper) LearningTypeNameByID(id string) string { func (m *inMemoryMapper) LearningTypeNameByID(id string) string {
return m.learningTypeByID[id] return m.learningTypeByID[id]
} }

View File

@ -0,0 +1,60 @@
package adapters
import (
"context"
"log/slog"
"os"
"time"
"git.loyso.art/frx/kurious/internal/common/config"
"git.loyso.art/frx/kurious/migrations/sqlite"
"github.com/stretchr/testify/suite"
)
type sqliteBaseSuite struct {
suite.Suite
// TODO: make baseTestSuite that provides this kind of things
ctx context.Context
cancel context.CancelFunc
log *slog.Logger
connection *sqliteConnection
}
func (s *sqliteBaseSuite) SetupSuite() {
s.ctx, s.cancel = context.WithCancel(context.Background())
s.log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: false,
Level: slog.LevelDebug,
}))
connection, err := NewSqliteConnection(s.ctx, config.Sqlite{
DSN: ":memory:",
ShutdownTimeout: time.Second * 3,
}, s.log)
s.Require().NoError(err)
s.connection = connection
db := s.connection.db
err = sqlite.RunMigrations(s.ctx, db.DB, s.log.With(slog.String("component", "migrator")))
s.Require().NoError(err)
}
func (s *sqliteBaseSuite) TearDownSuite() {
s.cancel()
err := s.connection.Close()
s.Require().NoError(err)
}
func (s *sqliteBaseSuite) TearDownTest() {
db := s.connection.db
for _, query := range []string{
"DELETE FROM learning_categories",
"DELETE FROM courses",
} {
_, err := db.ExecContext(s.ctx, query)
s.Require().NoError(err, "cleaning up database")
}
}

View File

@ -0,0 +1,54 @@
package adapters
import (
"context"
"fmt"
"log/slog"
"time"
"git.loyso.art/frx/kurious/internal/common/config"
"git.loyso.art/frx/kurious/migrations/sqlite"
"git.loyso.art/frx/kurious/pkg/xdefault"
"go.opentelemetry.io/otel/attribute"
"github.com/jmoiron/sqlx"
_ "modernc.org/sqlite"
)
type sqliteConnection struct {
db *sqlx.DB
shutdownTimeout time.Duration
log *slog.Logger
}
func NewSqliteConnection(ctx context.Context, cfg config.Sqlite, log *slog.Logger) (*sqliteConnection, error) {
conn, err := sqlx.Open("sqlite", cfg.DSN)
if err != nil {
return nil, fmt.Errorf("openning db connection: %w", err)
}
err = sqlite.RunMigrations(ctx, conn.DB, log)
if err != nil {
return nil, fmt.Errorf("running migrations: %w", err)
}
return &sqliteConnection{
db: conn,
log: log,
shutdownTimeout: xdefault.WithFallback(cfg.ShutdownTimeout, defaultShutdownTimeout),
}, nil
}
func (c *sqliteConnection) Close() error {
_, cancel := context.WithTimeout(context.Background(), c.shutdownTimeout)
defer cancel()
return c.db.Close()
}
func getSqliteBaseAttributes() []attribute.KeyValue {
return []attribute.KeyValue{
dbSystemAttr.String("sqlite"),
dbNameAttr.String("courses"),
}
}

View File

@ -0,0 +1,565 @@
package adapters
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"strings"
"time"
"git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/common/xcontext"
"git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/domain"
"github.com/jmoiron/sqlx"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func (c *sqliteConnection) CourseRepository() *sqliteCourseRepository {
return &sqliteCourseRepository{
db: c.db,
log: c.log.With(slog.String("repository", "course")),
}
}
type sqliteCourseRepository struct {
db *sqlx.DB
log *slog.Logger
}
func (r *sqliteCourseRepository) List(
ctx context.Context,
params domain.ListCoursesParams,
) (result domain.ListCoursesResult, err error) {
const queryTemplate = `SELECT %s from courses WHERE 1=1`
ctx, span := dbTracer.Start(
ctx, "list courses.courses",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
args := make([]any, 0, 6)
if params.LearningType != "" {
args = append(args, params.LearningType)
query += " AND learning_type = ?"
}
if params.CourseThematic != "" {
args = append(args, params.CourseThematic)
query += " AND course_thematic = ?"
}
if params.OrganizationID != "" {
args = append(args, params.OrganizationID)
query += " AND organization_id = ?"
}
if params.OrderBy == "" {
params.OrderBy = "id"
}
direction := "ASC"
if !params.Ascending {
direction = "DESC"
}
query += " ORDER BY " + params.OrderBy + " " + direction
if params.Limit > 0 {
query += " LIMIT ?"
args = append(args, params.Limit)
}
if params.Offset > 0 {
query += " OFFSET ?"
args = append(args, params.Offset)
}
span.SetAttributes(dbStatementAttr.String(query))
scanF := func(s rowsScanner) (err error) {
var cdb sqliteCourseDB
err = s.StructScan(&cdb)
if err != nil {
return err
}
result.Courses = append(result.Courses, cdb.AsDomain())
return nil
}
err = scanRows(ctx, r.db, scanF, query, args...)
if err != nil {
return result, err
}
if params.Limit > 0 && len(result.Courses) == params.Limit {
lastIDx := len(result.Courses) - 1
result.NextPageToken = result.Courses[lastIDx].ID
}
result.Count, err = r.listCount(ctx, params)
if err != nil {
xcontext.LogWithWarnError(ctx, r.log, err, "unable to list count")
}
span.SetAttributes(
attribute.Int("db.items_count", len(result.Courses)),
attribute.Int("db.total_items", result.Count),
)
return result, nil
}
func (r *sqliteCourseRepository) ListLearningTypes(
ctx context.Context,
) (result domain.ListLearningTypeResult, err error) {
const query = "SELECT DISTINCT learning_type FROM courses"
ctx, span := dbTracer.Start(
ctx, "list_learning_types courses.courses",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
err = r.db.SelectContext(ctx, &result.LearningTypeIDs, query)
if err != nil {
return result, fmt.Errorf("executing query: %w", err)
}
return result, nil
}
func (r *sqliteCourseRepository) ListCourseThematics(
ctx context.Context,
params domain.ListCourseThematicsParams,
) (result domain.ListCourseThematicsResult, err error) {
const queryTemplate = "SELECT DISTINCT course_thematic FROM courses WHERE 1=1"
query := queryTemplate
args := make([]any, 0, 1)
if params.LearningTypeID != "" {
args = append(args, params.LearningTypeID)
query += " AND learning_type = ?"
}
ctx, span := dbTracer.Start(
ctx, "list courses.course_thematic",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
err = r.db.SelectContext(ctx, &result.CourseThematicIDs, query, args...)
if err != nil {
return result, fmt.Errorf("executing query: %w", err)
}
return result, nil
}
func (r *sqliteCourseRepository) ListStatistics(
ctx context.Context,
params domain.ListStatisticsParams,
) (result domain.ListStatisticsResult, err error) {
const queryTemplate = `SELECT learning_type, course_thematic, organization_id, count(id) as count` +
` FROM courses` +
` WHERE 1=1`
query := queryTemplate
args := make([]any, 0, 3)
if params.LearningTypeID != "" {
query += ` AND learning_type = ?`
args = append(args, params.LearningTypeID)
}
if params.CourseThematicID != "" {
query += ` AND course_thematic = ?`
args = append(args, params.CourseThematicID)
}
if params.OrganizaitonID != "" {
query += ` AND organization_id = ?`
args = append(args, params.OrganizaitonID)
}
query += ` GROUP BY learning_type, course_thematic, organization_id`
query += ` ORDER BY count(id) DESC`
ctx, span := dbTracer.Start(
ctx, "list courses.statistics",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
dbArgumentsAttr.StringSlice(argumentsAsStrings(args...)),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
} else {
span.SetStatus(codes.Ok, "finished")
}
span.End()
}()
var stats []sqliteCourseStatistic
err = r.db.SelectContext(ctx, &stats, query, args...)
if err != nil {
return result, fmt.Errorf("executing query: %w", err)
}
result.LearningTypeStatistics = xslices.Map(stats, asDomainFunc)
return result, nil
}
func (r *sqliteCourseRepository) Get(
ctx context.Context,
id string,
) (course domain.Course, err error) {
const queryTemplate = `SELECT %s FROM courses WHERE id = ?`
query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
ctx, span := dbTracer.Start(
ctx, "get courses.courses",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
var courseDB sqliteCourseDB
err = r.db.GetContext(ctx, &courseDB, query, id)
if err != nil {
return course, fmt.Errorf("executing query: %w", err)
}
return courseDB.AsDomain(), nil
}
func (r *sqliteCourseRepository) GetByExternalID(
ctx context.Context, id string,
) (course domain.Course, err error) {
return course, errors.New("not implemented")
}
func (r *sqliteCourseRepository) CreateBatch(ctx context.Context, params ...domain.CreateCourseParams) error {
tx, err := r.db.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelDefault})
if err != nil {
return fmt.Errorf("beginning tx: %w", err)
}
defer func() {
var errTx error
if err != nil {
errTx = tx.Rollback()
} else {
errTx = tx.Commit()
}
err = errors.Join(err, errTx)
}()
const queryTempalate = `INSERT INTO courses` +
` (%s) VALUES (%s)`
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(coursesFields)), ",")
query := fmt.Sprintf(queryTempalate, coursesFieldsStr, placeholders)
stmt, err := tx.PrepareContext(ctx, query)
if err != nil {
return fmt.Errorf("preparing statement: %w", err)
}
for _, param := range params {
_, err := stmt.ExecContext(ctx, createCourseParamsAsValues(param)...)
if err != nil {
return fmt.Errorf("executing statement query: %w", err)
}
}
return nil
}
func (r *sqliteCourseRepository) Create(ctx context.Context, params domain.CreateCourseParams) (domain.Course, error) {
err := r.CreateBatch(ctx, params)
return domain.Course{}, err
}
func (r *sqliteCourseRepository) UpdateCourseDescription(ctx context.Context, id, description string) error {
return errors.New("unimplemented")
}
func (r *sqliteCourseRepository) Delete(ctx context.Context, id string) error {
return errors.New("unimplemented")
}
func (r *sqliteCourseRepository) listCount(ctx context.Context, params domain.ListCoursesParams) (count int, err error) {
const queryTemplate = `SELECT COUNT(id) FROM courses WHERE 1=1`
query := queryTemplate
args := make([]any, 0, 6)
if params.LearningType != "" {
args = append(args, params.LearningType)
query += " AND learning_type = ?"
}
if params.CourseThematic != "" {
args = append(args, params.CourseThematic)
query += " AND course_thematic = ?"
}
if params.OrganizationID != "" {
args = append(args, params.OrganizationID)
query += " AND organization_id = ?"
}
ctx, span := dbTracer.Start(
ctx, "list_count courses.courses",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
err = r.db.GetContext(ctx, &count, query, args...)
if err != nil {
return count, fmt.Errorf("sending query: %w", err)
}
return count, nil
}
type rowsScanner interface {
sqlx.ColScanner
StructScan(dest any) error
}
func scanRows(ctx context.Context, db *sqlx.DB, f func(rowsScanner) error, query string, args ...any) error {
rows, err := db.QueryxContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("querying rows: %w", err)
}
defer func() {
err = errors.Join(err, rows.Close())
}()
for rows.Next() {
err = f(rows)
if err != nil {
return fmt.Errorf("scanning row: %w", err)
}
}
if err = rows.Err(); err != nil {
return fmt.Errorf("checking rows for errors: %w", err)
}
return nil
}
func createCourseParamsAsValues(params domain.CreateCourseParams) []any {
now := time.Now()
return []any{
params.ID,
nullableValueAsString(params.ExternalID),
mapSourceTypeFromDomain(params.SourceType),
nullableValueAsString(params.SourceName),
params.CourseThematic,
params.LearningType,
params.OrganizationID,
params.OriginLink,
params.ImageLink,
params.Name,
params.Description,
params.FullPrice,
params.Discount,
params.Duration.Truncate(time.Second).Milliseconds() / 1000,
params.StartsAt,
now,
now,
sql.NullTime{},
}
}
type sqliteCourseDB struct {
ID string `db:"id"`
ExternalID sql.NullString `db:"external_id"`
SourceType string `db:"source_type"`
SourceName sql.NullString `db:"source_name"`
ThematicID string `db:"course_thematic"`
LearningTypeID string `db:"learning_type"`
OrganizationID string `db:"organization_id"`
OriginLink string `db:"origin_link"`
ImageLink string `db:"image_link"`
Name string `db:"name"`
Description string `db:"description"`
FullPrice float64 `db:"full_price"`
Discount float64 `db:"discount"`
Duration int64 `db:"duration"`
CreatedAt time.Time `db:"created_at"`
StartsAt sql.NullTime `db:"starts_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt sql.NullTime `db:"deleted_at"`
}
type sqliteCourseStatistic struct {
LearningTypeID string `db:"learning_type"`
CourseThematicID string `db:"course_thematic"`
OrganizationID string `db:"organization_id"`
Count int `db:"count"`
}
func (s sqliteCourseStatistic) AsDomain() domain.StatisticUnit {
return domain.StatisticUnit(s)
}
func nullStringAsDomain(s sql.NullString) nullable.Value[string] {
if s.Valid {
return nullable.NewValue(s.String)
}
return nullable.Value[string]{}
}
func nullTimeAsDomain(s sql.NullTime) nullable.Value[time.Time] {
if s.Valid {
return nullable.NewValue(s.Time)
}
return nullable.Value[time.Time]{}
}
func nullableValueAsString(v nullable.Value[string]) sql.NullString {
return sql.NullString{
Valid: v.Valid(),
String: v.Value(),
}
}
func nullableValueAsTime(v nullable.Value[time.Time]) sql.NullTime {
return sql.NullTime{
Valid: v.Valid(),
Time: v.Value(),
}
}
func (c sqliteCourseDB) AsDomain() domain.Course {
return domain.Course{
ID: c.ID,
OrganizationID: c.OrganizationID,
OriginLink: c.OriginLink,
ImageLink: c.ImageLink,
Name: c.Name,
Description: c.Description,
FullPrice: c.FullPrice,
Discount: c.Discount,
ThematicID: c.ThematicID,
LearningTypeID: c.LearningTypeID,
Duration: time.Second * time.Duration(c.Duration),
StartsAt: c.StartsAt.Time,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
ExternalID: nullStringAsDomain(c.ExternalID),
SourceType: mapSourceTypeToDomain(c.SourceType),
SourceName: nullStringAsDomain(c.SourceName),
DeletedAt: nullTimeAsDomain(c.DeletedAt),
}
}
func (c *sqliteCourseDB) FromDomain(d domain.Course) {
*c = sqliteCourseDB{
ID: d.ID,
OrganizationID: d.OrganizationID,
OriginLink: d.OriginLink,
ImageLink: d.ImageLink,
Name: d.Name,
Description: d.Description,
FullPrice: d.FullPrice,
Discount: d.Discount,
ThematicID: d.ThematicID,
LearningTypeID: d.LearningTypeID,
SourceType: mapSourceTypeFromDomain(d.SourceType),
Duration: d.Duration.Truncate(time.Second).Milliseconds() / 1000,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
ExternalID: nullableValueAsString(d.ExternalID),
SourceName: nullableValueAsString(d.SourceName),
DeletedAt: nullableValueAsTime(d.DeletedAt),
StartsAt: sql.NullTime{
Time: d.StartsAt,
Valid: true,
},
}
}
func (c *sqliteCourseRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue {
outbase := append(
getSqliteBaseAttributes(),
dbTableAttr.String("courses"),
)
return append(outbase, custom...)
}
func argumentsAsStrings(args ...any) []string {
out := make([]string, 0, len(args))
for _, arg := range args {
out = append(out, fmt.Sprintf("%v", arg))
}
return out
}

View File

@ -0,0 +1,107 @@
package adapters
import (
"strconv"
"testing"
"time"
"git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/kurious/domain"
"github.com/stretchr/testify/suite"
)
func TestSqliteCourseRepository(t *testing.T) {
suite.Run(t, new(sqliteCourseRepositorySuite))
}
type sqliteCourseRepositorySuite struct {
sqliteBaseSuite
}
func (s *sqliteCourseRepositorySuite) TestCreateCourse() {
expcourse := domain.Course{
ID: "test-id",
ExternalID: nullable.NewValue("ext-id"),
Name: "test-name",
SourceType: domain.SourceTypeParsed,
SourceName: nullable.NewValue("test-source"),
ThematicID: "test-thematic",
LearningTypeID: "test-learning",
OrganizationID: "test-org-id",
OriginLink: "test-link",
ImageLink: "test-image-link",
Description: "description",
FullPrice: 123,
Discount: 321,
Duration: time.Second * 360,
StartsAt: time.Date(2020, 10, 01, 11, 22, 33, 0, time.UTC),
Thematic: "",
LearningType: "",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nullable.Value[time.Time]{},
}
cr := s.connection.CourseRepository()
_, err := cr.Create(s.ctx, domain.CreateCourseParams{
ID: "test-id",
ExternalID: nullable.NewValue("ext-id"),
Name: "test-name",
SourceType: domain.SourceTypeParsed,
SourceName: nullable.NewValue("test-source"),
CourseThematic: "test-thematic",
LearningType: "test-learning",
OrganizationID: "test-org-id",
OriginLink: "test-link",
ImageLink: "test-image-link",
Description: "description",
FullPrice: 123,
Discount: 321,
Duration: time.Second * 360,
StartsAt: time.Date(2020, 10, 01, 11, 22, 33, 0, time.UTC),
})
s.Require().NoError(err)
gotCourse, err := cr.Get(s.ctx, expcourse.ID)
s.Require().NoError(err)
s.Require().NotEmpty(gotCourse.CreatedAt)
s.Require().NotEmpty(gotCourse.UpdatedAt)
s.Require().Empty(gotCourse.DeletedAt)
expcourse.CreatedAt = gotCourse.CreatedAt
expcourse.UpdatedAt = gotCourse.UpdatedAt
s.Require().Equal(expcourse, gotCourse)
}
func (s *sqliteCourseRepositorySuite) TestListLimitOffset() {
const coursecount = 9
const listparts = 3
basecourse := domain.CreateCourseParams{
SourceType: domain.SourceTypeManual,
}
cr := s.connection.CourseRepository()
for i := 0; i < coursecount; i++ {
basecourse.ID = strconv.Itoa(i)
_, err := cr.Create(s.ctx, basecourse)
s.NoError(err)
}
params := domain.ListCoursesParams{
Limit: coursecount / listparts,
}
for i := 0; i < listparts; i++ {
result, err := cr.List(s.ctx, params)
s.NoError(err)
s.Len(result.Courses, listparts)
params.Offset += listparts
}
result, err := cr.List(s.ctx, params)
s.NoError(err)
s.Empty(result.Courses)
}

View File

@ -0,0 +1,174 @@
package adapters
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"strings"
"git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/domain"
"github.com/jmoiron/sqlx"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var (
learningCategoryColumns = []string{
"id",
"logo",
"courses_count",
}
learningCategoryColumnsStr = joinColumns(learningCategoryColumns)
)
type learningCategoryDB struct {
ID string `db:"id"`
Logo sql.NullString `db:"logo"`
CoursesCount int `db:"courses_count"`
}
func (l learningCategoryDB) AsDomain() domain.LearningCategory {
return domain.LearningCategory{
ID: l.ID,
Logo: nullStringAsDomain(l.Logo),
CoursesCount: l.CoursesCount,
}
}
func (c *sqliteConnection) LearningCategory() *sqliteLearingCategoryRepository {
return &sqliteLearingCategoryRepository{
db: c.db,
log: c.log.With("repository", "learning_categories"),
}
}
type sqliteLearingCategoryRepository struct {
db *sqlx.DB
log *slog.Logger
}
func (r *sqliteLearingCategoryRepository) Upsert(ctx context.Context, c domain.LearningCategory) (err error) {
const queryTemplate = "INSERT INTO learning_categories (%s)" +
" VALUES (%s)" +
" ON CONFLICT(id) DO UPDATE" +
" SET id = excluded.id" +
", logo = excluded.logo" +
", courses_count = excluded.courses_count"
query := fmt.Sprintf(
queryTemplate,
learningCategoryColumnsStr,
strings.TrimSuffix(strings.Repeat("?,", len(learningCategoryColumns)), ","),
)
ctx, span := dbTracer.Start(
ctx, "upsert courses.learning_categories",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("INSERT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
_, err = r.db.ExecContext(
ctx, query,
c.ID,
nullableValueAsString(c.Logo),
c.CoursesCount,
)
if err != nil {
return fmt.Errorf("executing query: %w", err)
}
return nil
}
func (r *sqliteLearingCategoryRepository) List(ctx context.Context) (out []domain.LearningCategory, err error) {
const queryTemplate = "SELECT %s FROM learning_categories;"
query := fmt.Sprintf(queryTemplate, learningCategoryColumnsStr)
ctx, span := dbTracer.Start(
ctx, "list courses.learning_categories",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
var categories []learningCategoryDB
err = r.db.SelectContext(ctx, &categories, query)
if err != nil {
return nil, fmt.Errorf("executing query: %w", err)
}
out = xslices.Map(categories, asDomainFunc)
return out, nil
}
func (r *sqliteLearingCategoryRepository) Get(ctx context.Context, id string) (category domain.LearningCategory, err error) {
const queryTemplate = "SELECT %s FROM learning_categories WHERE id = ?;"
query := fmt.Sprintf(queryTemplate, learningCategoryColumnsStr)
ctx, span := dbTracer.Start(
ctx, "get courses.learning_categories",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
var cdb learningCategoryDB
err = r.db.GetContext(ctx, &cdb, query, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.LearningCategory{}, domain.ErrNotFound
}
return domain.LearningCategory{}, fmt.Errorf("executing query: %w", err)
}
return cdb.AsDomain(), nil
}
func (c *sqliteLearingCategoryRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue {
outbase := append(
getSqliteBaseAttributes(),
dbTableAttr.String("learning_categories"),
)
return append(outbase, custom...)
}

View File

@ -0,0 +1,101 @@
package adapters
import (
"testing"
"git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/kurious/domain"
"github.com/stretchr/testify/suite"
)
func TestSqliteLearningCategories(t *testing.T) {
suite.Run(t, new(sqliteLearningCategoriesRepositorySuite))
}
type sqliteLearningCategoriesRepositorySuite struct {
sqliteBaseSuite
}
func (s *sqliteLearningCategoriesRepositorySuite) TestGet() {
var cdb learningCategoryDB
err := s.connection.db.GetContext(
s.ctx, &cdb,
"INSERT INTO learning_categories (id, logo, courses_count) VALUES (?,?,?) RETURNING *",
"test-id", "test-url-logo", 42,
)
s.Require().NoError(err)
expectedCategory := cdb.AsDomain()
lr := s.connection.LearningCategory()
got, err := lr.Get(s.ctx, expectedCategory.ID)
s.NoError(err)
s.Equal(expectedCategory, got)
}
func (s *sqliteLearningCategoriesRepositorySuite) TestList() {
stmt, err := s.connection.db.PrepareContext(s.ctx, "INSERT INTO learning_categories (id, logo, courses_count) VALUES (?,?,?)")
s.NoError(err)
for _, args := range [][]any{
{"test-id-1", "test-url-1", 1},
{"test-id-2", "test-url-2", 2},
} {
_, err = stmt.ExecContext(s.ctx, args...)
s.NoError(err)
}
gotCategories, err := s.connection.LearningCategory().List(s.ctx)
s.NoError(err)
s.Len(gotCategories, 2)
expCategories := []domain.LearningCategory{
{
ID: "test-id-1",
Logo: nullable.NewValue("test-url-1"),
CoursesCount: 1,
},
{
ID: "test-id-2",
Logo: nullable.NewValue("test-url-2"),
CoursesCount: 2,
},
}
if gotCategories[0].ID != "test-id-1" {
gotCategories[0], gotCategories[1] = gotCategories[1], gotCategories[0]
}
s.ElementsMatch(expCategories, gotCategories)
}
func (s *sqliteLearningCategoriesRepositorySuite) TestUpsert() {
const categoryID = "test-id-1"
repo := s.connection.LearningCategory()
gotCategory, err := repo.Get(s.ctx, categoryID)
s.ErrorIs(err, domain.ErrNotFound)
s.Empty(gotCategory)
createdCategory := domain.LearningCategory{
ID: categoryID,
Logo: nullable.NewValue("test-url-1"),
CoursesCount: 1,
}
err = repo.Upsert(s.ctx, createdCategory)
s.NoError(err)
gotCategory, err = repo.Get(s.ctx, categoryID)
s.NoError(err)
s.Equal(createdCategory, gotCategory)
createdCategory.Logo = nullable.NewValue("test-url-2")
err = repo.Upsert(s.ctx, createdCategory)
s.NoError(err)
gotCategory, err = repo.Get(s.ctx, categoryID)
s.NoError(err)
s.Equal(createdCategory, gotCategory)
}

View File

@ -0,0 +1,329 @@
package adapters
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"strings"
"time"
"git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/domain"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/jmoiron/sqlx"
)
var (
organizationColumns = []string{
"id",
"external_id",
"alias",
"name",
"site",
"logo",
"created_at",
"updated_at",
"deleted_at",
}
organizationColumnsStr = joinColumns(organizationColumns)
organizationColumnsArgsStr = namedArgColumns(organizationColumns)
)
type organizationStatDB struct {
ID string `db:"id"`
ExternalID sql.NullString `db:"external_id"`
Name string `db:"name"`
CoursesCount uint64 `db:"courses_count"`
}
func (s organizationStatDB) AsDomain() domain.OrganizationStat {
return domain.OrganizationStat{
ID: s.ID,
ExternalID: nullStringAsDomain(s.ExternalID),
Name: s.Name,
CoursesCount: s.CoursesCount,
}
}
type organizationDB struct {
ID string `db:"id"`
ExternalID sql.NullString `db:"external_id"`
Alias string `db:"alias"`
Name string `db:"name"`
Site string `db:"site"`
Logo string `db:"logo"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt sql.NullTime `db:"deleted_at"`
}
func (o organizationDB) AsDomain() domain.Organization {
return domain.Organization{
ID: o.ID,
ExternalID: nullStringAsDomain(o.ExternalID),
Alias: o.Alias,
Name: o.Name,
Site: o.Site,
LogoLink: o.Logo,
CreatedAt: o.CreatedAt,
UpdatedAt: o.UpdatedAt,
DeletedAt: nullTimeAsDomain(o.DeletedAt),
}
}
func (o *organizationDB) FromDomain(in domain.Organization) {
*o = organizationDB{
ID: in.ID,
ExternalID: nullableValueAsString(in.ExternalID),
Alias: in.Alias,
Name: in.Name,
Site: in.Site,
Logo: in.LogoLink,
CreatedAt: in.CreatedAt,
UpdatedAt: in.UpdatedAt,
DeletedAt: nullableValueAsTime(in.DeletedAt),
}
}
func (c *sqliteConnection) Organization() domain.OrganizationRepository {
return &sqliteOrganizationRepository{
db: c.db,
log: c.log.With("repository", "organization"),
}
}
type sqliteOrganizationRepository struct {
db *sqlx.DB
log *slog.Logger
}
func (r *sqliteOrganizationRepository) ListStats(
ctx context.Context,
params domain.ListOrganizationsParams,
) (out []domain.OrganizationStat, err error) {
const queryTemplate = `WITH cte as (
SELECT learning_type, course_thematic, organization_id, count(id) as courses_count
FROM courses
WHERE 1=1 {whereSuffix}
GROUP BY learning_type, course_thematic, organization_id
) SELECT o.id as id, o.external_id as external_id, o.name as name, cte.courses_count as courses_count
FROM cte
INNER JOIN organizations o ON o.id = cte.organization_id`
whereSuffix := ""
args := make([]any, 0, 3)
if params.LearningTypeID != "" {
whereSuffix += " AND learning_type = ?"
args = append(args, params.LearningTypeID)
}
if params.CourseThematicID != "" {
whereSuffix += " AND course_thematic = ?"
args = append(args, params.CourseThematicID)
}
query := strings.Replace(queryTemplate, "{whereSuffix}", whereSuffix, 1)
ctx, span := dbTracer.Start(
ctx, "list_stats courses",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
var stats []organizationStatDB
err = r.db.SelectContext(ctx, &stats, query, args...)
if err != nil {
return nil, fmt.Errorf("executing query: %w", err)
}
return xslices.Map(stats, asDomainFunc), nil
}
func (r *sqliteOrganizationRepository) List(ctx context.Context, params domain.ListOrganizationsParams) (out []domain.Organization, err error) {
const queryTemplate = `SELECT %s FROM organizations WHERE 1=1`
query := fmt.Sprintf(queryTemplate, organizationColumnsStr)
args := make([]any, 0, len(params.IDs))
if len(params.IDs) > 0 {
args = append(
args,
xslices.Map(params.IDs, func(t string) any { return t })...,
)
queryParam := strings.TrimSuffix(strings.Repeat("?,", len(args)), ",")
query += " AND id IN (" + queryParam + ")"
}
ctx, span := dbTracer.Start(
ctx, "list courses.organizations",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
organizations := make([]organizationDB, 0, 1<<8)
err = r.db.SelectContext(ctx, &organizations, query)
if err != nil {
return nil, fmt.Errorf("executing query: %w", err)
}
return xslices.Map(organizations, asDomainFunc), nil
}
func (r *sqliteOrganizationRepository) Get(ctx context.Context, params domain.GetOrganizationParams) (out domain.Organization, err error) {
const queryTemplate = "SELECT %s FROM organizations WHERE 1=1"
query := fmt.Sprintf(queryTemplate, organizationColumnsStr)
args := make([]any, 0, 2)
if params.ID.Valid() {
args = append(args, params.ID.Value())
query += " AND id = ?"
}
if params.ExternalID.Valid() {
args = append(args, params.ExternalID.Value())
query += " AND external_id = ?"
}
ctx, span := dbTracer.Start(
ctx, "get courses.organizations",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("SELECT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
var orgdb organizationDB
err = r.db.GetContext(ctx, &orgdb, query, args...)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return out, domain.ErrNotFound
}
return out, fmt.Errorf("executing query: %w", err)
}
return orgdb.AsDomain(), nil
}
func (r *sqliteOrganizationRepository) Create(ctx context.Context, params domain.CreateOrganizationParams) (out domain.Organization, err error) {
const queryTemplate = `INSERT INTO organizations (%[1]s) VALUES (%[2]s) RETURNING %[1]s`
query := fmt.Sprintf(queryTemplate, organizationColumnsStr, organizationColumnsArgsStr)
ctx, span := dbTracer.Start(
ctx, "create courses.organizations",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("INSERT"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
stmt, err := r.db.PrepareNamedContext(ctx, query)
if err != nil {
return out, fmt.Errorf("preparing statement: %w", err)
}
var orgdb organizationDB
now := time.Now().UTC().Truncate(time.Second)
err = stmt.GetContext(ctx, &orgdb, organizationDB{
ID: params.ID,
ExternalID: nullableValueAsString(params.ExternalID),
Alias: params.Alias,
Name: params.Name,
Site: params.Site,
Logo: params.LogoLink,
CreatedAt: now,
UpdatedAt: now,
DeletedAt: sql.NullTime{},
})
if err != nil {
return out, fmt.Errorf("executing query: %w", err)
}
return orgdb.AsDomain(), nil
}
func (r *sqliteOrganizationRepository) Delete(ctx context.Context, id string) (err error) {
const query = `DELETE FROM organizations WHERE id = ?`
ctx, span := dbTracer.Start(
ctx, "delete courses.organizations",
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
r.mergeAttributes(
dbOperationAttr.String("DELETE"),
dbStatementAttr.String(query),
)...,
),
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.ErrNotFound
}
return fmt.Errorf("executing query: %w", err)
}
affected, _ := result.RowsAffected()
if affected == 0 {
return domain.ErrNotFound
}
return nil
}
func (c *sqliteOrganizationRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue {
outbase := append(
getSqliteBaseAttributes(),
dbTableAttr.String("organizaitons"),
)
return append(outbase, custom...)
}

View File

@ -0,0 +1,128 @@
package adapters
import (
"slices"
"strconv"
"testing"
"git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/kurious/domain"
"github.com/stretchr/testify/suite"
)
func TestSqilteOrganizations(t *testing.T) {
suite.Run(t, new(sqliteOrganzationRepositorySuite))
}
type sqliteOrganzationRepositorySuite struct {
sqliteBaseSuite
}
func (s *sqliteOrganzationRepositorySuite) TearDownTest() {
_ = s.connection.db.MustExecContext(s.ctx, "DELETE FROM organizations")
}
func (s *sqliteOrganzationRepositorySuite) TestList() {
const itemscount = 3
orgsdb := make([]domain.Organization, 0, itemscount)
baseOrg := domain.Organization{
Alias: "test-alias",
Name: "test-name",
Site: "test-site",
LogoLink: "test-logo",
}
for i := 0; i < itemscount; i++ {
nextitem := baseOrg
iStr := strconv.Itoa(i)
nextitem.ID = "test-id-" + iStr
nextitem.ExternalID.Set("test-ext-id-" + iStr)
gotOrg, err := s.connection.Organization().Create(s.ctx, domain.CreateOrganizationParams{
ID: nextitem.ID,
ExternalID: nextitem.ExternalID,
Alias: nextitem.Alias,
Name: nextitem.Name,
Site: nextitem.Site,
LogoLink: nextitem.LogoLink,
})
s.NoError(err)
orgsdb = append(orgsdb, gotOrg)
}
gotOrgs, err := s.connection.Organization().List(s.ctx, domain.ListOrganizationsParams{})
s.NoError(err)
compareF := func(lhs, rhs domain.Organization) int {
if lhs.ID < rhs.ID {
return -1
} else if lhs.ID > rhs.ID {
return 1
} else {
return 0
}
}
slices.SortFunc(gotOrgs, compareF)
for i := range gotOrgs {
s.NotEmpty(gotOrgs[i].CreatedAt)
s.NotEmpty(gotOrgs[i].UpdatedAt)
s.Empty(gotOrgs[i].DeletedAt)
orgsdb[i].CreatedAt = gotOrgs[i].CreatedAt
orgsdb[i].UpdatedAt = gotOrgs[i].UpdatedAt
orgsdb[i].DeletedAt = gotOrgs[i].DeletedAt
}
s.ElementsMatch(orgsdb, gotOrgs)
}
func (s *sqliteOrganzationRepositorySuite) TestGet() {
var orgdb organizationDB
err := s.connection.db.GetContext(
s.ctx, &orgdb,
`INSERT INTO organizations (`+organizationColumnsStr+`)`+
` VALUES ("test-id", "test-ext-id", "test-alias", "test-name", "test-site", "test-logo", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL)`+
` RETURNING `+organizationColumnsStr,
)
s.NoError(err)
expectedOrganization := orgdb.AsDomain()
getParams := domain.GetOrganizationParams{
ID: nullable.NewValue(orgdb.ID),
}
gotOrganization, err := s.connection.Organization().Get(s.ctx, getParams)
s.NoError(err)
s.Equal(expectedOrganization, gotOrganization)
}
func (s *sqliteOrganzationRepositorySuite) TestCreate() {
expectedOrganization := domain.Organization{
ID: "test-id",
ExternalID: nullable.NewValue("test-ext-id"),
Alias: "test-alias",
Name: "test-name",
Site: "test-site",
LogoLink: "test-logo",
}
gotOrganization, err := s.connection.Organization().Create(s.ctx, domain.CreateOrganizationParams{
ID: expectedOrganization.ID,
ExternalID: expectedOrganization.ExternalID,
Alias: expectedOrganization.Alias,
Name: expectedOrganization.Name,
Site: expectedOrganization.Site,
LogoLink: expectedOrganization.LogoLink,
})
s.NoError(err)
s.NotEmpty(gotOrganization.CreatedAt)
s.NotEmpty(gotOrganization.UpdatedAt)
s.Empty(gotOrganization.DeletedAt)
expectedOrganization.CreatedAt = gotOrganization.CreatedAt
expectedOrganization.UpdatedAt = gotOrganization.UpdatedAt
s.Equal(expectedOrganization, gotOrganization)
}

View File

@ -109,6 +109,14 @@ func (conn *YDBConnection) Close() error {
return conn.Driver.Close(ctx) return conn.Driver.Close(ctx)
} }
func (conn *YDBConnection) Organization() domain.OrganizationRepository {
return domain.NotImplementedOrganizationRepository{}
}
func (conn *YDBConnection) LearningCategory() domain.LearningCategoryRepository {
return domain.NotImplementedLearningCategory{}
}
func (conn *YDBConnection) CourseRepository() *ydbCourseRepository { func (conn *YDBConnection) CourseRepository() *ydbCourseRepository {
return &ydbCourseRepository{ return &ydbCourseRepository{
db: conn.Driver, db: conn.Driver,
@ -170,7 +178,6 @@ func (r *ydbCourseRepository) List(
opts = append( opts = append(
opts, opts,
table.ValueParam("$id", types.TextValue(params.NextPageToken)),
table.ValueParam("$limit", types.Int32Value(int32(params.Limit))), table.ValueParam("$limit", types.Int32Value(int32(params.Limit))),
) )

View File

@ -10,6 +10,8 @@ type Commands struct {
InsertCourse command.CreateCourseHandler InsertCourse command.CreateCourseHandler
DeleteCourse command.DeleteCourseHandler DeleteCourse command.DeleteCourseHandler
UpdateCourseDescription command.UpdateCourseDescriptionHandler UpdateCourseDescription command.UpdateCourseDescriptionHandler
InsertOrganization command.CreateOrganizationHandler
} }
type Queries struct { type Queries struct {
@ -17,6 +19,11 @@ type Queries struct {
ListCourses query.ListCourseHandler ListCourses query.ListCourseHandler
ListLearningTypes query.ListLearningTypesHandler ListLearningTypes query.ListLearningTypesHandler
ListCourseThematics query.ListCourseThematicsHandler ListCourseThematics query.ListCourseThematicsHandler
ListCourseStatistics query.ListCoursesStatsHandler
ListOrganzations query.ListOrganizationsHandler
ListOrganizationsStats query.ListOrganizationsStatsHandler
GetOrganization query.GetOrganizationHandler
} }
type Application struct { type Application struct {

View File

@ -0,0 +1,53 @@
package command
import (
"context"
"fmt"
"log/slog"
"git.loyso.art/frx/kurious/internal/common/decorator"
"git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
type CreateOrganization struct {
ID string
ExternalID nullable.Value[string]
Alias string
Name string
Site string
Logo string
}
type CreateOrganizationHandler decorator.CommandHandler[CreateOrganization]
type createOrganizationHandler struct {
repo domain.OrganizationRepository
}
func NewCreateOrganizationHandler(
repo domain.OrganizationRepository,
log *slog.Logger,
) CreateOrganizationHandler {
h := createOrganizationHandler{
repo: repo,
}
return decorator.ApplyCommandDecorators(h, log)
}
func (h createOrganizationHandler) Handle(ctx context.Context, cmd CreateOrganization) error {
_, err := h.repo.Create(ctx, domain.CreateOrganizationParams{
ID: cmd.ID,
ExternalID: cmd.ExternalID,
Alias: cmd.Alias,
Name: cmd.Name,
Site: cmd.Site,
LogoLink: cmd.Logo,
})
if err != nil {
return fmt.Errorf("creating organization: %w", err)
}
return nil
}

View File

@ -0,0 +1,45 @@
package query
import (
"context"
"fmt"
"log/slog"
"git.loyso.art/frx/kurious/internal/common/decorator"
"git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
type GetOrganization struct {
ID nullable.Value[string]
ExternalID nullable.Value[string]
}
type GetOrganizationHandler decorator.QueryHandler[GetOrganization, domain.Organization]
type getOrganizationHandler struct {
repo domain.OrganizationRepository
}
func NewGetOrganizationHandler(
repo domain.OrganizationRepository,
log *slog.Logger,
) GetOrganizationHandler {
h := getOrganizationHandler{
repo: repo,
}
return decorator.AddQueryDecorators(h, log)
}
func (h getOrganizationHandler) Handle(ctx context.Context, query GetOrganization) (domain.Organization, error) {
organization, err := h.repo.Get(ctx, domain.GetOrganizationParams{
ID: query.ID,
ExternalID: query.ExternalID,
})
if err != nil {
return domain.Organization{}, fmt.Errorf("getting organization: %w", err)
}
return organization, nil
}

View File

@ -17,7 +17,11 @@ type ListCourse struct {
OrganizationID string OrganizationID string
Keyword string Keyword string
OrderBy string
Ascending bool
Limit int Limit int
Offset int
NextPageToken string NextPageToken string
} }
@ -41,10 +45,13 @@ func NewListCourseHandler(
} }
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) { func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) {
out.NextPageToken = query.NextPageToken const defaultBatchSize = 1_000
drainFull := query.Limit == 0 drainFull := query.Limit == 0
if !drainFull { if !drainFull {
out.Courses = make([]domain.Course, 0, query.Limit) out.Courses = make([]domain.Course, 0, query.Limit)
} else {
query.Limit = defaultBatchSize
} }
for { for {
result, err := h.repo.List(ctx, domain.ListCoursesParams{ result, err := h.repo.List(ctx, domain.ListCoursesParams{
@ -52,8 +59,9 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do
LearningType: query.LearningType, LearningType: query.LearningType,
OrganizationID: query.OrganizationID, OrganizationID: query.OrganizationID,
Limit: query.Limit, Limit: query.Limit,
Offset: query.Offset,
NextPageToken: out.NextPageToken, OrderBy: query.OrderBy,
Ascending: query.Ascending,
}) })
if err != nil { if err != nil {
return out, fmt.Errorf("listing courses: %w", err) return out, fmt.Errorf("listing courses: %w", err)
@ -67,9 +75,10 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do
}) })
out.Courses = append(out.Courses, result.Courses...) out.Courses = append(out.Courses, result.Courses...)
out.NextPageToken = result.NextPageToken out.Count = result.Count
if drainFull && len(result.Courses) > 0 && result.NextPageToken != "" { if drainFull && len(result.Courses) == query.Limit {
query.Offset += query.Limit
continue continue
} }

View File

@ -0,0 +1,69 @@
package query
import (
"context"
"fmt"
"log/slog"
"git.loyso.art/frx/kurious/internal/common/decorator"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
type ListCoursesStats struct {
LearningTypeID string
CourseThematicsID string
OrganizationID string
}
type ListCoursesStatsResult struct{}
type ListCoursesStatsHandler decorator.QueryHandler[ListCoursesStats, domain.ListCoursesStatsResult]
type listCoursesStatsHandler struct {
repo domain.CourseRepository
mapper domain.CourseMapper
}
func NewListCoursesStatsHandler(
mapper domain.CourseMapper,
repo domain.CourseRepository,
log *slog.Logger,
) ListCoursesStatsHandler {
h := listCoursesStatsHandler{
repo: repo,
mapper: mapper,
}
return decorator.AddQueryDecorators(h, log)
}
func (h listCoursesStatsHandler) Handle(
ctx context.Context,
query ListCoursesStats,
) (out domain.ListCoursesStatsResult, err error) {
if query.OrganizationID != "" {
statistics, err := h.repo.ListStatistics(ctx, domain.ListStatisticsParams{
LearningTypeID: query.LearningTypeID,
CourseThematicID: query.CourseThematicsID,
OrganizaitonID: query.OrganizationID,
})
if err != nil {
return out, fmt.Errorf("listing statistics: %w", err)
}
out.StatsByLearningType = make(map[string]domain.LearningTypeStat, len(statistics.LearningTypeStatistics))
for _, unit := range statistics.LearningTypeStatistics {
stats, ok := out.StatsByLearningType[unit.LearningTypeID]
stats.Count++
if !ok {
stats.CourseThematic = make(map[string]int)
}
stats.CourseThematic[unit.CourseThematicID]++
out.StatsByLearningType[unit.LearningTypeID] = stats
}
} else {
stats := h.mapper.GetStats(false)
out.StatsByLearningType = stats
}
return out, nil
}

View File

@ -0,0 +1,42 @@
package query
import (
"context"
"fmt"
"log/slog"
"git.loyso.art/frx/kurious/internal/common/decorator"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
type ListOrganizations struct {
IDs []string
}
type ListOrganizationsHandler decorator.QueryHandler[ListOrganizations, []domain.Organization]
type listOrganizationsHandler struct {
repo domain.OrganizationRepository
}
func NewListOrganizationsHandler(
repo domain.OrganizationRepository,
log *slog.Logger,
) ListOrganizationsHandler {
h := listOrganizationsHandler{
repo: repo,
}
return decorator.AddQueryDecorators(h, log)
}
func (h listOrganizationsHandler) Handle(ctx context.Context, query ListOrganizations) ([]domain.Organization, error) {
organizations, err := h.repo.List(ctx, domain.ListOrganizationsParams{
IDs: query.IDs,
})
if err != nil {
return nil, fmt.Errorf("listing organizations: %w", err)
}
return organizations, nil
}

View File

@ -0,0 +1,52 @@
package query
import (
"context"
"fmt"
"log/slog"
"git.loyso.art/frx/kurious/internal/common/decorator"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
type ListOrganizationsStats struct {
LearningTypeID string
CourseThematicID string
IDs []string
}
type ListOrganizationsStatsHandler decorator.QueryHandler[
ListOrganizationsStats,
[]domain.OrganizationStat,
]
type listOrganizationsStatsHandler struct {
repo domain.OrganizationRepository
}
func NewListOrganizationsStatsHandler(
repo domain.OrganizationRepository,
log *slog.Logger,
) ListOrganizationsStatsHandler {
h := listOrganizationsStatsHandler{
repo: repo,
}
return decorator.AddQueryDecorators(h, log)
}
func (h listOrganizationsStatsHandler) Handle(
ctx context.Context,
query ListOrganizationsStats,
) ([]domain.OrganizationStat, error) {
stats, err := h.repo.ListStats(ctx, domain.ListOrganizationsParams{
LearningTypeID: query.LearningTypeID,
CourseThematicID: query.CourseThematicID,
IDs: query.IDs,
})
if err != nil {
return nil, fmt.Errorf("listing stats: %w", err)
}
return stats, nil
}

View File

@ -0,0 +1,9 @@
package domain
import "git.loyso.art/frx/kurious/internal/common/nullable"
type LearningCategory struct {
ID string
Logo nullable.Value[string]
CoursesCount int
}

View File

@ -0,0 +1,12 @@
package domain
const (
ErrNotFound PlainError = "not found"
ErrNotImplemented PlainError = "not implemented"
)
type PlainError string
func (err PlainError) Error() string {
return string(err)
}

View File

@ -1,6 +1,12 @@
package domain package domain
import "context"
type CourseMapper interface { type CourseMapper interface {
CourseThematicNameByID(string) string CourseThematicNameByID(string) string
LearningTypeNameByID(string) string LearningTypeNameByID(string) string
CollectCounts(context.Context, CourseRepository) error
GetCounts(byCourseThematic, byLearningType string) int
GetStats(copyMap bool) map[string]LearningTypeStat
} }

View File

@ -0,0 +1,534 @@
// Code generated by mockery v2.42.1. DO NOT EDIT.
package mocks
import (
context "context"
domain "git.loyso.art/frx/kurious/internal/kurious/domain"
mock "github.com/stretchr/testify/mock"
)
// CourseRepository is an autogenerated mock type for the CourseRepository type
type CourseRepository struct {
mock.Mock
}
type CourseRepository_Expecter struct {
mock *mock.Mock
}
func (_m *CourseRepository) EXPECT() *CourseRepository_Expecter {
return &CourseRepository_Expecter{mock: &_m.Mock}
}
// Create provides a mock function with given fields: _a0, _a1
func (_m *CourseRepository) Create(_a0 context.Context, _a1 domain.CreateCourseParams) (domain.Course, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 domain.Course
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, domain.CreateCourseParams) (domain.Course, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, domain.CreateCourseParams) domain.Course); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Get(0).(domain.Course)
}
if rf, ok := ret.Get(1).(func(context.Context, domain.CreateCourseParams) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CourseRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
type CourseRepository_Create_Call struct {
*mock.Call
}
// Create is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 domain.CreateCourseParams
func (_e *CourseRepository_Expecter) Create(_a0 interface{}, _a1 interface{}) *CourseRepository_Create_Call {
return &CourseRepository_Create_Call{Call: _e.mock.On("Create", _a0, _a1)}
}
func (_c *CourseRepository_Create_Call) Run(run func(_a0 context.Context, _a1 domain.CreateCourseParams)) *CourseRepository_Create_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(domain.CreateCourseParams))
})
return _c
}
func (_c *CourseRepository_Create_Call) Return(_a0 domain.Course, _a1 error) *CourseRepository_Create_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *CourseRepository_Create_Call) RunAndReturn(run func(context.Context, domain.CreateCourseParams) (domain.Course, error)) *CourseRepository_Create_Call {
_c.Call.Return(run)
return _c
}
// CreateBatch provides a mock function with given fields: _a0, _a1
func (_m *CourseRepository) CreateBatch(_a0 context.Context, _a1 ...domain.CreateCourseParams) error {
_va := make([]interface{}, len(_a1))
for _i := range _a1 {
_va[_i] = _a1[_i]
}
var _ca []interface{}
_ca = append(_ca, _a0)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
if len(ret) == 0 {
panic("no return value specified for CreateBatch")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, ...domain.CreateCourseParams) error); ok {
r0 = rf(_a0, _a1...)
} else {
r0 = ret.Error(0)
}
return r0
}
// CourseRepository_CreateBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBatch'
type CourseRepository_CreateBatch_Call struct {
*mock.Call
}
// CreateBatch is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 ...domain.CreateCourseParams
func (_e *CourseRepository_Expecter) CreateBatch(_a0 interface{}, _a1 ...interface{}) *CourseRepository_CreateBatch_Call {
return &CourseRepository_CreateBatch_Call{Call: _e.mock.On("CreateBatch",
append([]interface{}{_a0}, _a1...)...)}
}
func (_c *CourseRepository_CreateBatch_Call) Run(run func(_a0 context.Context, _a1 ...domain.CreateCourseParams)) *CourseRepository_CreateBatch_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]domain.CreateCourseParams, len(args)-1)
for i, a := range args[1:] {
if a != nil {
variadicArgs[i] = a.(domain.CreateCourseParams)
}
}
run(args[0].(context.Context), variadicArgs...)
})
return _c
}
func (_c *CourseRepository_CreateBatch_Call) Return(_a0 error) *CourseRepository_CreateBatch_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *CourseRepository_CreateBatch_Call) RunAndReturn(run func(context.Context, ...domain.CreateCourseParams) error) *CourseRepository_CreateBatch_Call {
_c.Call.Return(run)
return _c
}
// Delete provides a mock function with given fields: ctx, id
func (_m *CourseRepository) Delete(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// CourseRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type CourseRepository_Delete_Call struct {
*mock.Call
}
// Delete is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *CourseRepository_Expecter) Delete(ctx interface{}, id interface{}) *CourseRepository_Delete_Call {
return &CourseRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, id)}
}
func (_c *CourseRepository_Delete_Call) Run(run func(ctx context.Context, id string)) *CourseRepository_Delete_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *CourseRepository_Delete_Call) Return(_a0 error) *CourseRepository_Delete_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *CourseRepository_Delete_Call) RunAndReturn(run func(context.Context, string) error) *CourseRepository_Delete_Call {
_c.Call.Return(run)
return _c
}
// Get provides a mock function with given fields: ctx, id
func (_m *CourseRepository) Get(ctx context.Context, id string) (domain.Course, error) {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 domain.Course
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (domain.Course, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string) domain.Course); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Get(0).(domain.Course)
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CourseRepository_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
type CourseRepository_Get_Call struct {
*mock.Call
}
// Get is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *CourseRepository_Expecter) Get(ctx interface{}, id interface{}) *CourseRepository_Get_Call {
return &CourseRepository_Get_Call{Call: _e.mock.On("Get", ctx, id)}
}
func (_c *CourseRepository_Get_Call) Run(run func(ctx context.Context, id string)) *CourseRepository_Get_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *CourseRepository_Get_Call) Return(_a0 domain.Course, _a1 error) *CourseRepository_Get_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *CourseRepository_Get_Call) RunAndReturn(run func(context.Context, string) (domain.Course, error)) *CourseRepository_Get_Call {
_c.Call.Return(run)
return _c
}
// GetByExternalID provides a mock function with given fields: ctx, id
func (_m *CourseRepository) GetByExternalID(ctx context.Context, id string) (domain.Course, error) {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for GetByExternalID")
}
var r0 domain.Course
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (domain.Course, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string) domain.Course); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Get(0).(domain.Course)
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CourseRepository_GetByExternalID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByExternalID'
type CourseRepository_GetByExternalID_Call struct {
*mock.Call
}
// GetByExternalID is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *CourseRepository_Expecter) GetByExternalID(ctx interface{}, id interface{}) *CourseRepository_GetByExternalID_Call {
return &CourseRepository_GetByExternalID_Call{Call: _e.mock.On("GetByExternalID", ctx, id)}
}
func (_c *CourseRepository_GetByExternalID_Call) Run(run func(ctx context.Context, id string)) *CourseRepository_GetByExternalID_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *CourseRepository_GetByExternalID_Call) Return(_a0 domain.Course, _a1 error) *CourseRepository_GetByExternalID_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *CourseRepository_GetByExternalID_Call) RunAndReturn(run func(context.Context, string) (domain.Course, error)) *CourseRepository_GetByExternalID_Call {
_c.Call.Return(run)
return _c
}
// List provides a mock function with given fields: _a0, _a1
func (_m *CourseRepository) List(_a0 context.Context, _a1 domain.ListCoursesParams) (domain.ListCoursesResult, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for List")
}
var r0 domain.ListCoursesResult
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, domain.ListCoursesParams) (domain.ListCoursesResult, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, domain.ListCoursesParams) domain.ListCoursesResult); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Get(0).(domain.ListCoursesResult)
}
if rf, ok := ret.Get(1).(func(context.Context, domain.ListCoursesParams) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CourseRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
type CourseRepository_List_Call struct {
*mock.Call
}
// List is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 domain.ListCoursesParams
func (_e *CourseRepository_Expecter) List(_a0 interface{}, _a1 interface{}) *CourseRepository_List_Call {
return &CourseRepository_List_Call{Call: _e.mock.On("List", _a0, _a1)}
}
func (_c *CourseRepository_List_Call) Run(run func(_a0 context.Context, _a1 domain.ListCoursesParams)) *CourseRepository_List_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(domain.ListCoursesParams))
})
return _c
}
func (_c *CourseRepository_List_Call) Return(_a0 domain.ListCoursesResult, _a1 error) *CourseRepository_List_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *CourseRepository_List_Call) RunAndReturn(run func(context.Context, domain.ListCoursesParams) (domain.ListCoursesResult, error)) *CourseRepository_List_Call {
_c.Call.Return(run)
return _c
}
// ListCourseThematics provides a mock function with given fields: _a0, _a1
func (_m *CourseRepository) ListCourseThematics(_a0 context.Context, _a1 domain.ListCourseThematicsParams) (domain.ListCourseThematicsResult, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for ListCourseThematics")
}
var r0 domain.ListCourseThematicsResult
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, domain.ListCourseThematicsParams) (domain.ListCourseThematicsResult, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, domain.ListCourseThematicsParams) domain.ListCourseThematicsResult); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Get(0).(domain.ListCourseThematicsResult)
}
if rf, ok := ret.Get(1).(func(context.Context, domain.ListCourseThematicsParams) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CourseRepository_ListCourseThematics_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListCourseThematics'
type CourseRepository_ListCourseThematics_Call struct {
*mock.Call
}
// ListCourseThematics is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 domain.ListCourseThematicsParams
func (_e *CourseRepository_Expecter) ListCourseThematics(_a0 interface{}, _a1 interface{}) *CourseRepository_ListCourseThematics_Call {
return &CourseRepository_ListCourseThematics_Call{Call: _e.mock.On("ListCourseThematics", _a0, _a1)}
}
func (_c *CourseRepository_ListCourseThematics_Call) Run(run func(_a0 context.Context, _a1 domain.ListCourseThematicsParams)) *CourseRepository_ListCourseThematics_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(domain.ListCourseThematicsParams))
})
return _c
}
func (_c *CourseRepository_ListCourseThematics_Call) Return(_a0 domain.ListCourseThematicsResult, _a1 error) *CourseRepository_ListCourseThematics_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *CourseRepository_ListCourseThematics_Call) RunAndReturn(run func(context.Context, domain.ListCourseThematicsParams) (domain.ListCourseThematicsResult, error)) *CourseRepository_ListCourseThematics_Call {
_c.Call.Return(run)
return _c
}
// ListLearningTypes provides a mock function with given fields: _a0
func (_m *CourseRepository) ListLearningTypes(_a0 context.Context) (domain.ListLearningTypeResult, error) {
ret := _m.Called(_a0)
if len(ret) == 0 {
panic("no return value specified for ListLearningTypes")
}
var r0 domain.ListLearningTypeResult
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (domain.ListLearningTypeResult, error)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(context.Context) domain.ListLearningTypeResult); ok {
r0 = rf(_a0)
} else {
r0 = ret.Get(0).(domain.ListLearningTypeResult)
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CourseRepository_ListLearningTypes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListLearningTypes'
type CourseRepository_ListLearningTypes_Call struct {
*mock.Call
}
// ListLearningTypes is a helper method to define mock.On call
// - _a0 context.Context
func (_e *CourseRepository_Expecter) ListLearningTypes(_a0 interface{}) *CourseRepository_ListLearningTypes_Call {
return &CourseRepository_ListLearningTypes_Call{Call: _e.mock.On("ListLearningTypes", _a0)}
}
func (_c *CourseRepository_ListLearningTypes_Call) Run(run func(_a0 context.Context)) *CourseRepository_ListLearningTypes_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *CourseRepository_ListLearningTypes_Call) Return(_a0 domain.ListLearningTypeResult, _a1 error) *CourseRepository_ListLearningTypes_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *CourseRepository_ListLearningTypes_Call) RunAndReturn(run func(context.Context) (domain.ListLearningTypeResult, error)) *CourseRepository_ListLearningTypes_Call {
_c.Call.Return(run)
return _c
}
// UpdateCourseDescription provides a mock function with given fields: ctx, id, description
func (_m *CourseRepository) UpdateCourseDescription(ctx context.Context, id string, description string) error {
ret := _m.Called(ctx, id, description)
if len(ret) == 0 {
panic("no return value specified for UpdateCourseDescription")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, id, description)
} else {
r0 = ret.Error(0)
}
return r0
}
// CourseRepository_UpdateCourseDescription_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateCourseDescription'
type CourseRepository_UpdateCourseDescription_Call struct {
*mock.Call
}
// UpdateCourseDescription is a helper method to define mock.On call
// - ctx context.Context
// - id string
// - description string
func (_e *CourseRepository_Expecter) UpdateCourseDescription(ctx interface{}, id interface{}, description interface{}) *CourseRepository_UpdateCourseDescription_Call {
return &CourseRepository_UpdateCourseDescription_Call{Call: _e.mock.On("UpdateCourseDescription", ctx, id, description)}
}
func (_c *CourseRepository_UpdateCourseDescription_Call) Run(run func(ctx context.Context, id string, description string)) *CourseRepository_UpdateCourseDescription_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *CourseRepository_UpdateCourseDescription_Call) Return(_a0 error) *CourseRepository_UpdateCourseDescription_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *CourseRepository_UpdateCourseDescription_Call) RunAndReturn(run func(context.Context, string, string) error) *CourseRepository_UpdateCourseDescription_Call {
_c.Call.Return(run)
return _c
}
// NewCourseRepository creates a new instance of CourseRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewCourseRepository(t interface {
mock.TestingT
Cleanup(func())
}) *CourseRepository {
mock := &CourseRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,199 @@
// Code generated by mockery v2.42.1. DO NOT EDIT.
package mocks
import (
context "context"
domain "git.loyso.art/frx/kurious/internal/kurious/domain"
mock "github.com/stretchr/testify/mock"
)
// LearningCategoryRepository is an autogenerated mock type for the LearningCategoryRepository type
type LearningCategoryRepository struct {
mock.Mock
}
type LearningCategoryRepository_Expecter struct {
mock *mock.Mock
}
func (_m *LearningCategoryRepository) EXPECT() *LearningCategoryRepository_Expecter {
return &LearningCategoryRepository_Expecter{mock: &_m.Mock}
}
// Get provides a mock function with given fields: _a0, _a1
func (_m *LearningCategoryRepository) Get(_a0 context.Context, _a1 string) (domain.LearningCategory, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 domain.LearningCategory
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (domain.LearningCategory, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, string) domain.LearningCategory); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Get(0).(domain.LearningCategory)
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// LearningCategoryRepository_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
type LearningCategoryRepository_Get_Call struct {
*mock.Call
}
// Get is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 string
func (_e *LearningCategoryRepository_Expecter) Get(_a0 interface{}, _a1 interface{}) *LearningCategoryRepository_Get_Call {
return &LearningCategoryRepository_Get_Call{Call: _e.mock.On("Get", _a0, _a1)}
}
func (_c *LearningCategoryRepository_Get_Call) Run(run func(_a0 context.Context, _a1 string)) *LearningCategoryRepository_Get_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *LearningCategoryRepository_Get_Call) Return(_a0 domain.LearningCategory, _a1 error) *LearningCategoryRepository_Get_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *LearningCategoryRepository_Get_Call) RunAndReturn(run func(context.Context, string) (domain.LearningCategory, error)) *LearningCategoryRepository_Get_Call {
_c.Call.Return(run)
return _c
}
// List provides a mock function with given fields: _a0
func (_m *LearningCategoryRepository) List(_a0 context.Context) ([]domain.LearningCategory, error) {
ret := _m.Called(_a0)
if len(ret) == 0 {
panic("no return value specified for List")
}
var r0 []domain.LearningCategory
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) ([]domain.LearningCategory, error)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(context.Context) []domain.LearningCategory); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]domain.LearningCategory)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// LearningCategoryRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
type LearningCategoryRepository_List_Call struct {
*mock.Call
}
// List is a helper method to define mock.On call
// - _a0 context.Context
func (_e *LearningCategoryRepository_Expecter) List(_a0 interface{}) *LearningCategoryRepository_List_Call {
return &LearningCategoryRepository_List_Call{Call: _e.mock.On("List", _a0)}
}
func (_c *LearningCategoryRepository_List_Call) Run(run func(_a0 context.Context)) *LearningCategoryRepository_List_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *LearningCategoryRepository_List_Call) Return(_a0 []domain.LearningCategory, _a1 error) *LearningCategoryRepository_List_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *LearningCategoryRepository_List_Call) RunAndReturn(run func(context.Context) ([]domain.LearningCategory, error)) *LearningCategoryRepository_List_Call {
_c.Call.Return(run)
return _c
}
// Upsert provides a mock function with given fields: _a0, _a1
func (_m *LearningCategoryRepository) Upsert(_a0 context.Context, _a1 domain.LearningCategory) error {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Upsert")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, domain.LearningCategory) error); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Error(0)
}
return r0
}
// LearningCategoryRepository_Upsert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upsert'
type LearningCategoryRepository_Upsert_Call struct {
*mock.Call
}
// Upsert is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 domain.LearningCategory
func (_e *LearningCategoryRepository_Expecter) Upsert(_a0 interface{}, _a1 interface{}) *LearningCategoryRepository_Upsert_Call {
return &LearningCategoryRepository_Upsert_Call{Call: _e.mock.On("Upsert", _a0, _a1)}
}
func (_c *LearningCategoryRepository_Upsert_Call) Run(run func(_a0 context.Context, _a1 domain.LearningCategory)) *LearningCategoryRepository_Upsert_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(domain.LearningCategory))
})
return _c
}
func (_c *LearningCategoryRepository_Upsert_Call) Return(_a0 error) *LearningCategoryRepository_Upsert_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *LearningCategoryRepository_Upsert_Call) RunAndReturn(run func(context.Context, domain.LearningCategory) error) *LearningCategoryRepository_Upsert_Call {
_c.Call.Return(run)
return _c
}
// NewLearningCategoryRepository creates a new instance of LearningCategoryRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewLearningCategoryRepository(t interface {
mock.TestingT
Cleanup(func())
}) *LearningCategoryRepository {
mock := &LearningCategoryRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -0,0 +1,256 @@
// Code generated by mockery v2.42.1. DO NOT EDIT.
package mocks
import (
context "context"
domain "git.loyso.art/frx/kurious/internal/kurious/domain"
mock "github.com/stretchr/testify/mock"
)
// OrganizationRepository is an autogenerated mock type for the OrganizationRepository type
type OrganizationRepository struct {
mock.Mock
}
type OrganizationRepository_Expecter struct {
mock *mock.Mock
}
func (_m *OrganizationRepository) EXPECT() *OrganizationRepository_Expecter {
return &OrganizationRepository_Expecter{mock: &_m.Mock}
}
// Create provides a mock function with given fields: _a0, _a1
func (_m *OrganizationRepository) Create(_a0 context.Context, _a1 domain.CreateOrganizationParams) (domain.Organization, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 domain.Organization
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, domain.CreateOrganizationParams) (domain.Organization, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, domain.CreateOrganizationParams) domain.Organization); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Get(0).(domain.Organization)
}
if rf, ok := ret.Get(1).(func(context.Context, domain.CreateOrganizationParams) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OrganizationRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
type OrganizationRepository_Create_Call struct {
*mock.Call
}
// Create is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 domain.CreateOrganizationParams
func (_e *OrganizationRepository_Expecter) Create(_a0 interface{}, _a1 interface{}) *OrganizationRepository_Create_Call {
return &OrganizationRepository_Create_Call{Call: _e.mock.On("Create", _a0, _a1)}
}
func (_c *OrganizationRepository_Create_Call) Run(run func(_a0 context.Context, _a1 domain.CreateOrganizationParams)) *OrganizationRepository_Create_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(domain.CreateOrganizationParams))
})
return _c
}
func (_c *OrganizationRepository_Create_Call) Return(_a0 domain.Organization, _a1 error) *OrganizationRepository_Create_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *OrganizationRepository_Create_Call) RunAndReturn(run func(context.Context, domain.CreateOrganizationParams) (domain.Organization, error)) *OrganizationRepository_Create_Call {
_c.Call.Return(run)
return _c
}
// Delete provides a mock function with given fields: ctx, id
func (_m *OrganizationRepository) Delete(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// OrganizationRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type OrganizationRepository_Delete_Call struct {
*mock.Call
}
// Delete is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *OrganizationRepository_Expecter) Delete(ctx interface{}, id interface{}) *OrganizationRepository_Delete_Call {
return &OrganizationRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, id)}
}
func (_c *OrganizationRepository_Delete_Call) Run(run func(ctx context.Context, id string)) *OrganizationRepository_Delete_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *OrganizationRepository_Delete_Call) Return(_a0 error) *OrganizationRepository_Delete_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *OrganizationRepository_Delete_Call) RunAndReturn(run func(context.Context, string) error) *OrganizationRepository_Delete_Call {
_c.Call.Return(run)
return _c
}
// Get provides a mock function with given fields: _a0, _a1
func (_m *OrganizationRepository) Get(_a0 context.Context, _a1 domain.GetOrganizationParams) (domain.Organization, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 domain.Organization
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, domain.GetOrganizationParams) (domain.Organization, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, domain.GetOrganizationParams) domain.Organization); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Get(0).(domain.Organization)
}
if rf, ok := ret.Get(1).(func(context.Context, domain.GetOrganizationParams) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OrganizationRepository_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
type OrganizationRepository_Get_Call struct {
*mock.Call
}
// Get is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 domain.GetOrganizationParams
func (_e *OrganizationRepository_Expecter) Get(_a0 interface{}, _a1 interface{}) *OrganizationRepository_Get_Call {
return &OrganizationRepository_Get_Call{Call: _e.mock.On("Get", _a0, _a1)}
}
func (_c *OrganizationRepository_Get_Call) Run(run func(_a0 context.Context, _a1 domain.GetOrganizationParams)) *OrganizationRepository_Get_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(domain.GetOrganizationParams))
})
return _c
}
func (_c *OrganizationRepository_Get_Call) Return(_a0 domain.Organization, _a1 error) *OrganizationRepository_Get_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *OrganizationRepository_Get_Call) RunAndReturn(run func(context.Context, domain.GetOrganizationParams) (domain.Organization, error)) *OrganizationRepository_Get_Call {
_c.Call.Return(run)
return _c
}
// List provides a mock function with given fields: _a0
func (_m *OrganizationRepository) List(_a0 context.Context) ([]domain.Organization, error) {
ret := _m.Called(_a0)
if len(ret) == 0 {
panic("no return value specified for List")
}
var r0 []domain.Organization
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) ([]domain.Organization, error)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(context.Context) []domain.Organization); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]domain.Organization)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OrganizationRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
type OrganizationRepository_List_Call struct {
*mock.Call
}
// List is a helper method to define mock.On call
// - _a0 context.Context
func (_e *OrganizationRepository_Expecter) List(_a0 interface{}) *OrganizationRepository_List_Call {
return &OrganizationRepository_List_Call{Call: _e.mock.On("List", _a0)}
}
func (_c *OrganizationRepository_List_Call) Run(run func(_a0 context.Context)) *OrganizationRepository_List_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *OrganizationRepository_List_Call) Return(_a0 []domain.Organization, _a1 error) *OrganizationRepository_List_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *OrganizationRepository_List_Call) RunAndReturn(run func(context.Context) ([]domain.Organization, error)) *OrganizationRepository_List_Call {
_c.Call.Return(run)
return _c
}
// NewOrganizationRepository creates a new instance of OrganizationRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewOrganizationRepository(t interface {
mock.TestingT
Cleanup(func())
}) *OrganizationRepository {
mock := &OrganizationRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

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

View File

@ -12,8 +12,16 @@ type ListCoursesParams struct {
CourseThematic string CourseThematic string
OrganizationID string OrganizationID string
NextPageToken string
Limit int Limit int
Offset int
OrderBy string
Ascending bool
}
type ListStatisticsParams struct {
LearningTypeID string
CourseThematicID string
OrganizaitonID string
} }
type CreateCourseParams struct { type CreateCourseParams struct {
@ -34,9 +42,19 @@ type CreateCourseParams struct {
StartsAt time.Time StartsAt time.Time
} }
type LearningTypeStat struct {
Count int
CourseThematic map[string]int
}
type ListCoursesStatsResult struct {
StatsByLearningType map[string]LearningTypeStat
}
type ListCoursesResult struct { type ListCoursesResult struct {
Courses []Course Courses []Course
NextPageToken string NextPageToken string
Count int
} }
type ListLearningTypeResult struct { type ListLearningTypeResult struct {
@ -51,12 +69,24 @@ type ListCourseThematicsResult struct {
CourseThematicIDs []string CourseThematicIDs []string
} }
type StatisticUnit struct {
LearningTypeID string
CourseThematicID string
OrganizationID string
Count int
}
type ListStatisticsResult struct {
LearningTypeStatistics []StatisticUnit
}
//go:generate mockery --name CourseRepository //go:generate mockery --name CourseRepository
type CourseRepository interface { type CourseRepository interface {
// List courses by specifid parameters. // List courses by specifid parameters.
List(context.Context, ListCoursesParams) (ListCoursesResult, error) List(context.Context, ListCoursesParams) (ListCoursesResult, error)
ListLearningTypes(context.Context) (ListLearningTypeResult, error) ListLearningTypes(context.Context) (ListLearningTypeResult, error)
ListCourseThematics(context.Context, ListCourseThematicsParams) (ListCourseThematicsResult, error) ListCourseThematics(context.Context, ListCourseThematicsParams) (ListCourseThematicsResult, error)
ListStatistics(context.Context, ListStatisticsParams) (ListStatisticsResult, error)
// Get course by id. // Get course by id.
// Should return ErrNotFound in case course not found. // Should return ErrNotFound in case course not found.
Get(ctx context.Context, id string) (Course, error) Get(ctx context.Context, id string) (Course, error)
@ -75,6 +105,11 @@ type CourseRepository interface {
UpdateCourseDescription(ctx context.Context, id string, description string) error UpdateCourseDescription(ctx context.Context, id string, description string) error
} }
type GetOrganizationParams struct {
ID nullable.Value[string]
ExternalID nullable.Value[string]
}
type CreateOrganizationParams struct { type CreateOrganizationParams struct {
ID string ID string
ExternalID nullable.Value[string] ExternalID nullable.Value[string]
@ -85,9 +120,59 @@ type CreateOrganizationParams struct {
LogoLink string LogoLink string
} }
type ListOrganizationsParams struct {
LearningTypeID string
CourseThematicID string
IDs []string
}
//go:generate mockery --name OrganizationRepository //go:generate mockery --name OrganizationRepository
type OrganizationRepository interface { type OrganizationRepository interface {
Get(ctx context.Context) (Organization, error) ListStats(context.Context, ListOrganizationsParams) ([]OrganizationStat, error)
List(context.Context, ListOrganizationsParams) ([]Organization, error)
Get(context.Context, GetOrganizationParams) (Organization, error)
Create(context.Context, CreateOrganizationParams) (Organization, error) Create(context.Context, CreateOrganizationParams) (Organization, error)
Delete(ctx context.Context, id string) error Delete(ctx context.Context, id string) error
} }
type NotImplementedOrganizationRepository struct{}
func (NotImplementedOrganizationRepository) ListStats(
context.Context,
ListOrganizationsParams,
) ([]OrganizationStat, error) {
return nil, ErrNotImplemented
}
func (NotImplementedOrganizationRepository) List(context.Context, ListOrganizationsParams) ([]Organization, error) {
return nil, ErrNotImplemented
}
func (NotImplementedOrganizationRepository) Get(context.Context, GetOrganizationParams) (Organization, error) {
return Organization{}, ErrNotImplemented
}
func (NotImplementedOrganizationRepository) Create(context.Context, CreateOrganizationParams) (Organization, error) {
return Organization{}, ErrNotImplemented
}
func (NotImplementedOrganizationRepository) Delete(ctx context.Context, id string) error {
return ErrNotImplemented
}
//go:generate mockery --name LearningCategoryRepository
type LearningCategoryRepository interface {
Upsert(context.Context, LearningCategory) error
List(context.Context) ([]LearningCategory, error)
Get(context.Context, string) (LearningCategory, error)
}
type NotImplementedLearningCategory struct{}
func (NotImplementedLearningCategory) Upsert(context.Context, LearningCategory) error {
return ErrNotImplemented
}
func (NotImplementedLearningCategory) List(context.Context) ([]LearningCategory, error) {
return nil, ErrNotImplemented
}
func (NotImplementedLearningCategory) Get(context.Context, string) (LearningCategory, error) {
return LearningCategory{}, ErrNotImplemented
}

View File

@ -35,6 +35,7 @@ type syncSravniHandler struct {
log *slog.Logger log *slog.Logger
knownExternalIDs map[string]struct{} knownExternalIDs map[string]struct{}
knownOrganizationsByExternalID map[string]struct{}
isRunning uint32 isRunning uint32
} }
@ -83,6 +84,7 @@ func (h *syncSravniHandler) Handle(ctx context.Context) (err error) {
learningTypes := state.Props.InitialReduxState.Dictionaries.Data.LearningType learningTypes := state.Props.InitialReduxState.Dictionaries.Data.LearningType
courses := make([]sravni.Course, 0, 1024) courses := make([]sravni.Course, 0, 1024)
buffer := make([]sravni.Course, 0, 512) buffer := make([]sravni.Course, 0, 512)
organizations := make([]sravni.Organization, 0, 256)
for _, learningType := range learningTypes.Fields { for _, learningType := range learningTypes.Fields {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -115,61 +117,98 @@ func (h *syncSravniHandler) Handle(ctx context.Context) (err error) {
// since count is known it might be optimized to allocate slice once per request. // since count is known it might be optimized to allocate slice once per request.
for _, courseThematic := range thematics { for _, courseThematic := range thematics {
buffer = buffer[:0] var filteredCourses int
buffer, err = h.loadEducationalProducts(lctx, learningType.Value, courseThematic, buffer) var filteredOrgs int
if err != nil {
if !errors.Is(err, context.Canceled) {
xcontext.LogWithWarnError(lctx, h.log, err, "unable to load educational products", slog.Int("count", len(thematics)))
continue
}
var orgsByID map[string]sravni.Organization
buffer = buffer[:0]
buffer, orgsByID, err = h.loadEducationalProducts(lctx, learningType.Value, courseThematic, buffer)
if err != nil {
if errors.Is(err, context.Canceled) {
return fmt.Errorf("loading educational products: %w", err) return fmt.Errorf("loading educational products: %w", err)
} }
xcontext.LogWithWarnError(lctx, h.log, err, "unable to load educational products", slog.Int("count", len(thematics)))
continue
}
xslices.ForEach(buffer, func(c sravni.Course) { xslices.ForEach(buffer, func(c sravni.Course) {
// TODO: if the same course appears in different categories, it should be handled
if !h.setCourseIfNotKnown(c) {
return
}
c.Learningtype = []string{learningType.Value} c.Learningtype = []string{learningType.Value}
c.CourseThematics = []string{courseThematic} c.CourseThematics = []string{courseThematic}
courses = append(courses, c) courses = append(courses, c)
filteredCourses++
}) })
xcontext.LogInfo(lctx, h.log, "parsed subitems", slog.String("course_thematic", courseThematic), slog.Int("amount", len(buffer))) for _, org := range orgsByID {
if !h.setOrganizationIfNotKnown(org) {
continue
}
organizations = append(organizations, org)
filteredOrgs++
}
xcontext.LogInfo(
lctx, h.log, "parsed subitems",
slog.String("course_thematic", courseThematic),
slog.Int("amount", len(buffer)),
slog.Int("new_courses", filteredCourses),
slog.Int("new_organizations", filteredOrgs),
)
} }
elapsed := time.Since(start) elapsed := time.Since(start)
xcontext.LogDebug(lctx, h.log, "parsed items", slog.Duration("elapsed", elapsed), slog.Int("amount", len(courses))) xcontext.LogDebug(lctx, h.log, "parsed items", slog.Duration("elapsed", elapsed), slog.Int("amount", len(courses)))
// TODO: if the same course appears in different categories, it should be handled xcontext.LogDebug(
courses = h.filterByCache(courses) lctx, h.log, "filtered items",
if len(courses) == 0 { slog.Int("courses", len(courses)),
xcontext.LogInfo(lctx, h.log, "all courses were filtered out") slog.Int("organizations", len(organizations)),
continue )
var insertCourseSuccess bool
if len(courses) > 0 {
err = h.insertCourses(lctx, courses)
if err != nil {
xcontext.LogWithError(lctx, h.log, err, "unable to insert courses")
} }
xcontext.LogDebug(lctx, h.log, "filtered items", slog.Int("amount", len(courses))) insertCourseSuccess = err == nil
}
var insertOrgsSuccess bool
if len(organizations) > 0 {
err = h.insertOrganizations(lctx, organizations)
if err != nil {
xcontext.LogWithError(lctx, h.log, err, "unable to insert courses")
}
insertOrgsSuccess = err == nil
}
err = h.insertValues(lctx, courses)
elapsed = time.Since(start) - elapsed elapsed = time.Since(start) - elapsed
elapsedField := slog.Duration("elapsed", elapsed) elapsedField := slog.Duration("elapsed", elapsed)
if err != nil {
xcontext.LogWithError(lctx, h.log, err, "unable to insert courses", elapsedField)
continue
}
xslices.ForEach(courses, func(c sravni.Course) {
h.knownExternalIDs[c.ID] = struct{}{}
})
xcontext.LogInfo( xcontext.LogInfo(
lctx, h.log, "processed items", lctx, h.log, "inserting finished",
elapsedField, elapsedField,
slog.Int("count", len(courses)), slog.Bool("courses_insert_success", insertCourseSuccess),
slog.Bool("organization_insert_success", insertOrgsSuccess),
slog.Int("courses_count", len(courses)),
slog.Int("organizations_count", len(organizations)),
) )
} }
return nil return nil
} }
func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learningType, courseThematic string, buf []sravni.Course) ([]sravni.Course, error) { func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learningType, courseThematic string, buf []sravni.Course) ([]sravni.Course, map[string]sravni.Organization, error) {
const maxDeepIteration = 10 const maxDeepIteration = 10
const defaultLimit = 50 const defaultLimit = 50
@ -177,6 +216,7 @@ func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learnin
rateLimit := rate.NewLimiter(rateStrategy, 1) rateLimit := rate.NewLimiter(rateStrategy, 1)
var courses []sravni.Course var courses []sravni.Course
var organizationsByID = make(map[string]sravni.Organization)
if buf == nil || cap(buf) == 0 { if buf == nil || cap(buf) == 0 {
courses = make([]sravni.Course, 0, 256) courses = make([]sravni.Course, 0, 256)
} else { } else {
@ -193,35 +233,49 @@ func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learnin
params.Offset = offset params.Offset = offset
response, err := h.client.ListEducationalProducts(ctx, params) response, err := h.client.ListEducationalProducts(ctx, params)
if err != nil { if err != nil {
return nil, fmt.Errorf("listing educational products: %w", err) return nil, nil, fmt.Errorf("listing educational products: %w", err)
} }
offset += defaultLimit offset += defaultLimit
courses = append(courses, response.Items...) courses = append(courses, response.Items...)
for oid, org := range response.Organizations {
organizationsByID[oid] = org
}
if len(response.Items) < defaultLimit { if len(response.Items) < defaultLimit {
break break
} }
err = rateLimit.Wait(ctx) err = rateLimit.Wait(ctx)
if err != nil { if err != nil {
return courses, fmt.Errorf("waiting for limit: %w", err) return courses, organizationsByID, fmt.Errorf("waiting for limit: %w", err)
} }
} }
return courses, nil return courses, organizationsByID, nil
} }
func (h *syncSravniHandler) filterByCache(courses []sravni.Course) (toInsert []sravni.Course) { func (h *syncSravniHandler) setCourseIfNotKnown(course sravni.Course) (set bool) {
toCut := xslices.FilterInplace(courses, xslices.Not(h.isCached))
return courses[:toCut]
}
func (h *syncSravniHandler) isCached(course sravni.Course) bool {
_, ok := h.knownExternalIDs[course.ID] _, ok := h.knownExternalIDs[course.ID]
return ok if !ok {
h.knownExternalIDs[course.ID] = struct{}{}
} }
func (h *syncSravniHandler) insertValues(ctx context.Context, courses []sravni.Course) error { return !ok
}
func (h *syncSravniHandler) setOrganizationIfNotKnown(organization sravni.Organization) bool {
_, ok := h.knownOrganizationsByExternalID[organization.ID]
if !ok {
h.knownOrganizationsByExternalID[organization.ID] = struct{}{}
}
return !ok
}
func (h *syncSravniHandler) insertCourses(ctx context.Context, courses []sravni.Course) error {
courseParams := xslices.Map(courses, courseAsCreateCourseParams) courseParams := xslices.Map(courses, courseAsCreateCourseParams)
err := h.svc.Commands.InsertCourses.Handle(ctx, command.CreateCourses{ err := h.svc.Commands.InsertCourses.Handle(ctx, command.CreateCourses{
Courses: courseParams, Courses: courseParams,
@ -233,7 +287,69 @@ func (h *syncSravniHandler) insertValues(ctx context.Context, courses []sravni.C
return nil return nil
} }
func (h *syncSravniHandler) insertOrganizations(ctx context.Context, organizations []sravni.Organization) error {
organizationParams := xslices.Map(organizations, func(in sravni.Organization) command.CreateOrganization {
return command.CreateOrganization{
ID: generator.RandomInt64ID(),
ExternalID: nullable.NewValue(in.ID),
Alias: in.Alias,
Name: in.Name.Short,
Site: "",
Logo: in.Logotypes.Web,
}
})
for _, params := range organizationParams {
err := h.svc.Commands.InsertOrganization.Handle(ctx, params)
if err != nil {
return fmt.Errorf("inserting organization: %w", err)
}
}
return nil
}
func (h *syncSravniHandler) fillOrganizaionCaches(ctx context.Context) error {
if h.knownOrganizationsByExternalID != nil {
xcontext.LogDebug(ctx, h.log, "organization cache already filled")
return nil
}
organizations, err := h.svc.Queries.ListOrganzations.Handle(ctx, query.ListOrganizations{})
if err != nil {
return fmt.Errorf("listing organizations: %w", err)
}
withExternalID := func(in domain.Organization) bool {
return in.ExternalID.Valid()
}
getExtID := func(in domain.Organization) string {
return in.ExternalID.Value()
}
h.knownOrganizationsByExternalID = xslices.AsMap(xslices.Filter(organizations, withExternalID), getExtID)
xcontext.LogInfo(ctx, h.log, "cache filled", slog.String("kind", "organizations_by_external_id"), slog.Int("count", len(organizations)))
return nil
}
func (h *syncSravniHandler) fillCaches(ctx context.Context) error { func (h *syncSravniHandler) fillCaches(ctx context.Context) error {
err := h.fillOrganizaionCaches(ctx)
if err != nil {
return err
}
err = h.fillKnownExternalIDsCache(ctx)
if err != nil {
return err
}
return nil
}
func (h *syncSravniHandler) fillKnownExternalIDsCache(ctx context.Context) error {
if h.knownExternalIDs != nil { if h.knownExternalIDs != nil {
xcontext.LogInfo(ctx, h.log, "cache already filled") xcontext.LogInfo(ctx, h.log, "cache already filled")
@ -256,7 +372,7 @@ func (h *syncSravniHandler) fillCaches(ctx context.Context) error {
h.knownExternalIDs[c.ExternalID.Value()] = struct{}{} h.knownExternalIDs[c.ExternalID.Value()] = struct{}{}
}) })
xcontext.LogInfo(ctx, h.log, "cache filled", slog.Int("count", len(courses))) xcontext.LogInfo(ctx, h.log, "cache filled", slog.String("kind", "courses_by_external_id"), slog.Int("count", len(courses)))
return nil return nil
} }

View File

@ -1,4 +1,4 @@
package templ package bootstrap
templ button(title string, attributes templ.Attributes) { templ button(title string, attributes templ.Attributes) {
<button class="button" { attributes... }>{ title }</button> <button class="button" { attributes... }>{ title }</button>
@ -11,7 +11,6 @@ templ buttonRedirect(id, title string, linkTo string) {
> >
{ title } { title }
</button> </button>
@onclickRedirect("origin-link-" + id, linkTo) @onclickRedirect("origin-link-" + id, linkTo)
} }

View File

@ -1,7 +1,7 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.513 // templ: version: v0.2.707
package templ package bootstrap
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@ -38,7 +38,7 @@ func button(title string, attributes templ.Attributes) templ.Component {
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/common.templ`, Line: 3, Col: 49} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/common.templ`, Line: 4, Col: 49}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -72,7 +72,12 @@ func buttonRedirect(id, title string, linkTo string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("origin-link-" + id)) var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("origin-link-" + id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/common.templ`, Line: 10, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -80,12 +85,12 @@ func buttonRedirect(id, title string, linkTo string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/common.templ`, Line: 11, Col: 8} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/common.templ`, Line: 12, Col: 9}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -106,11 +111,12 @@ func buttonRedirect(id, title string, linkTo string) templ.Component {
func onclickRedirect(id, to string) templ.ComponentScript { func onclickRedirect(id, to string) templ.ComponentScript {
return templ.ComponentScript{ return templ.ComponentScript{
Name: `__templ_onclickRedirect_47ae`, Name: `__templ_onclickRedirect_5c43`,
Function: `function __templ_onclickRedirect_47ae(id, to){document.getElementById(id).onclick = () => { Function: `function __templ_onclickRedirect_5c43(id, to){document.getElementById(id).onclick = () => {
location.href = to location.href = to
}}`, }
Call: templ.SafeScript(`__templ_onclickRedirect_47ae`, id, to), }`,
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_47ae`, id, to), Call: templ.SafeScript(`__templ_onclickRedirect_5c43`, id, to),
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_5c43`, id, to),
} }
} }

View File

@ -0,0 +1,236 @@
package bootstrap
templ head(title string) {
<head>
<title>{ title }</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"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous">
</script>
</head>
}
templ headerNavbar(page PageKind) {
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
<div class="container-fluid">
<a class="navbar-brand" href="/htmlexamples/index.html">Kurious</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a
class={ "nav-link", templ.KV("active", page == PageIndex) }
aria-current="page"
href="/"
>
Home
</a>
</li>
<li class="nav-item">
<a
class={ "nav-link", templ.KV("active", page == PageCourses) }
aria-current="page"
href="/courses"
>
Courses
</a>
</li>
<li class="nav-item">
<a
class={ "nav-link", templ.KV("active", page == PageAbout) }
href="/about"
>
About us
</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
}
templ footer() {
<footer class="text-center text-lg-start bg-body-tertiary text-muted">
<section class="p-2">
<div class="container text-center text-md-start mt-5">
<div class="row mt-3">
<div class="col-md-3 col-lg-4 col-xl-3 mx-auto mb-4">
<h6 class="text-uppercase fw-bold mb-4">
<i class="fas fa-gem me-3"></i>Courses
</h6>
<p>
Welcome to Courses, your gateway to learning! Explore a diverse
range of courses and advance your skills with us. Join our
community and transform your life through education.
</p>
</div>
<div class="col-md-3 col-lg-2 col-xl-2 mx-auto mb-4">
<h6 class="text-uppercase fw-bold mb-4">Useful links</h6>
<p>
<a href="#!" class="text-reset">Pricing</a>
</p>
<p>
<a href="#!" class="text-reset">Settings</a>
</p>
<p>
<a href="#!" class="text-reset">Orders</a>
</p>
<p>
<a href="#!" class="text-reset">Help</a>
</p>
</div>
<div class="col-md-4 col-lg-3 col-xl-3 mx-auto mb-md-0 mb-4">
<h6 class="text-uppercase fw-bold mb-4">Contact</h6>
<p><i class="fas fa-home me-3"></i> New York, NY 10012, US</p>
<p>
<i class="fas fa-envelope me-3"></i>
info@example.com
</p>
<p><i class="fas fa-phone me-3"></i> + 01 234 567 88</p>
<p><i class="fas fa-print me-3"></i> + 01 234 567 89</p>
</div>
</div>
</div>
</section>
<div class="text-center p-4" style="background-color: rgba(0, 0, 0, 0.05)">
© 2024 Copyright:
<a class="text-reset fw-bold" href="https://mdbootstrap.com/">kursov.net</a>
</div>
</footer>
}
script elementScriptsLoad() {
const loadInputValues = () => {
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 base_url = (ct !== null && ct.value !== '') ? `${prefix}/${ct.value}` : prefix;
const school_selector = document.getElementById('schoolSelect');
const school_id = (school_selector !== null && school_selector.value !== '') ? school_selector.value : '';
const order_by = document.getElementById('sortBySelect');
const order_by_value = (order_by !== null && order_by.value !== '') ? order_by.value : '';
const ascending = document.getElementById('sortByOrder');
params = [];
if (school_id) {
params.push(`school_id=${school_id}`);
};
if (order_by_value) {
params.push(`order_by=${order_by_value}`);
};
if (ascending && ascending.checked) {
params.push(`asc=true`);
};
const final_url = base_url + "?" + params.join("&");
return {
base_url: base_url,
final_url: final_url,
school_id: school_id,
order_by: order_by_value,
ascending: ascending,
}
};
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;
values = loadInputValues();
const out = values.final_url;
document.location.assign(out);
return false;
};
const filterBySchool = () => {
/*
const school_selector = document.getElementById('schoolSelect');
const school_id = (school_selector !== null && school_selector.value !== '') ? school_selector.value : '';
const order_by = document.getElementById('sortBySelect');
const order_by_value = (order_by !== null && order_by.value !== '') ? order_by.value : '';
const ascending = document.getElementById('sortByOrder');
const baseUrl = `${window.location.pathname}?`;
params = [];
if (school_id) {
params.push(`school_id=${school_id}`);
};
if (order_by_value) {
params.push(`order_by=${order_by_value}`);
};
if (ascending && ascending.checked) {
params.push(`asc=true`);
};
const finalUrl = baseUrl + params.join("&");
*/
values = loadInputValues();
const finalUrl = values.final_url;
if (history.pushState) {
// history.pushState(null, null, finalUrl);
// TODO: remove once htmx implemented
window.location.assign(finalUrl);
} else {
window.location.assign(finalUrl);
};
}
document.addEventListener('DOMContentLoaded', () => {
const ff = document.getElementById('filter-form');
if (ff) ff.addEventListener('submit', formFilterOnSubmit);
const fs = document.getElementById('schoolSelect');
if (fs) fs.onchange = filterBySchool;
const ob = document.getElementById('sortBySelect');
if (ob) ob.onchange = filterBySchool;
});
}
templ root(page PageKind, _ stats) {
<!DOCTYPE html>
<html lang="ru">
@head(string(page))
<body>
@headerNavbar(page)
<div class="container">
{ children... }
</div>
@elementScriptsLoad()
</body>
@footer()
</html>
}

View File

@ -0,0 +1,331 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
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"
func head(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)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head><title>")
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/bootstrap/core.templ`, Line: 5, Col: 16}
}
_, 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("</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\"><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css\"><script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL\" crossorigin=\"anonymous\">\n\t\t</script></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 headerNavbar(page PageKind) 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("<header><nav class=\"navbar navbar-expand-lg bg-body-tertiary w-auto\"><div class=\"container-fluid\"><a class=\"navbar-brand\" href=\"/htmlexamples/index.html\">Kurious</a> <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarSupportedContent\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Toggle navigation\"><span class=\"navbar-toggler-icon\"></span></button><div class=\"collapse navbar-collapse\" id=\"navbarSupportedContent\"><ul class=\"navbar-nav me-auto mb-2 mb-lg-0\"><li class=\"nav-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 = []any{"nav-link", templ.KV("active", page == PageIndex)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/core.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-current=\"page\" href=\"/\">Home</a></li><li class=\"nav-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 = []any{"nav-link", templ.KV("active", page == PageCourses)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var6).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/core.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-current=\"page\" href=\"/courses\">Courses</a></li><li class=\"nav-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 = []any{"nav-link", templ.KV("active", page == PageAbout)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/core.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" href=\"/about\">About us</a></li></ul></div></div></nav></header>")
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_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer class=\"text-center text-lg-start bg-body-tertiary text-muted\"><section class=\"p-2\"><div class=\"container text-center text-md-start mt-5\"><div class=\"row mt-3\"><div class=\"col-md-3 col-lg-4 col-xl-3 mx-auto mb-4\"><h6 class=\"text-uppercase fw-bold mb-4\"><i class=\"fas fa-gem me-3\"></i>Courses</h6><p>Welcome to Courses, your gateway to learning! Explore a diverse range of courses and advance your skills with us. Join our community and transform your life through education.</p></div><div class=\"col-md-3 col-lg-2 col-xl-2 mx-auto mb-4\"><h6 class=\"text-uppercase fw-bold mb-4\">Useful links</h6><p><a href=\"#!\" class=\"text-reset\">Pricing</a></p><p><a href=\"#!\" class=\"text-reset\">Settings</a></p><p><a href=\"#!\" class=\"text-reset\">Orders</a></p><p><a href=\"#!\" class=\"text-reset\">Help</a></p></div><div class=\"col-md-4 col-lg-3 col-xl-3 mx-auto mb-md-0 mb-4\"><h6 class=\"text-uppercase fw-bold mb-4\">Contact</h6><p><i class=\"fas fa-home me-3\"></i> New York, NY 10012, US</p><p><i class=\"fas fa-envelope me-3\"></i> info@example.com</p><p><i class=\"fas fa-phone me-3\"></i> + 01 234 567 88</p><p><i class=\"fas fa-print me-3\"></i> + 01 234 567 89</p></div></div></div></section><div class=\"text-center p-4\" style=\"background-color: rgba(0, 0, 0, 0.05)\">© 2024 Copyright: <a class=\"text-reset fw-bold\" href=\"https://mdbootstrap.com/\">kursov.net</a></div></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
})
}
func elementScriptsLoad() templ.ComponentScript {
return templ.ComponentScript{
Name: `__templ_elementScriptsLoad_bfb1`,
Function: `function __templ_elementScriptsLoad_bfb1(){const loadInputValues = () => {
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 base_url = (ct !== null && ct.value !== '') ? ` + "`" + `${prefix}/${ct.value}` + "`" + ` : prefix;
const school_selector = document.getElementById('schoolSelect');
const school_id = (school_selector !== null && school_selector.value !== '') ? school_selector.value : '';
const order_by = document.getElementById('sortBySelect');
const order_by_value = (order_by !== null && order_by.value !== '') ? order_by.value : '';
const ascending = document.getElementById('sortByOrder');
params = [];
if (school_id) {
params.push(` + "`" + `school_id=${school_id}` + "`" + `);
};
if (order_by_value) {
params.push(` + "`" + `order_by=${order_by_value}` + "`" + `);
};
if (ascending && ascending.checked) {
params.push(` + "`" + `asc=true` + "`" + `);
};
const final_url = base_url + "?" + params.join("&");
return {
base_url: base_url,
final_url: final_url,
school_id: school_id,
order_by: order_by_value,
ascending: ascending,
}
};
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;
values = loadInputValues();
const out = values.final_url;
document.location.assign(out);
return false;
};
const filterBySchool = () => {
/*
const school_selector = document.getElementById('schoolSelect');
const school_id = (school_selector !== null && school_selector.value !== '') ? school_selector.value : '';
const order_by = document.getElementById('sortBySelect');
const order_by_value = (order_by !== null && order_by.value !== '') ? order_by.value : '';
const ascending = document.getElementById('sortByOrder');
const baseUrl = ` + "`" + `${window.location.pathname}?` + "`" + `;
params = [];
if (school_id) {
params.push(` + "`" + `school_id=${school_id}` + "`" + `);
};
if (order_by_value) {
params.push(` + "`" + `order_by=${order_by_value}` + "`" + `);
};
if (ascending && ascending.checked) {
params.push(` + "`" + `asc=true` + "`" + `);
};
const finalUrl = baseUrl + params.join("&");
*/
values = loadInputValues();
const finalUrl = values.final_url;
if (history.pushState) {
// history.pushState(null, null, finalUrl);
// TODO: remove once htmx implemented
window.location.assign(finalUrl);
} else {
window.location.assign(finalUrl);
};
}
document.addEventListener('DOMContentLoaded', () => {
const ff = document.getElementById('filter-form');
if (ff) ff.addEventListener('submit', formFilterOnSubmit);
const fs = document.getElementById('schoolSelect');
if (fs) fs.onchange = filterBySchool;
const ob = document.getElementById('sortBySelect');
if (ob) ob.onchange = filterBySchool;
});
}`,
Call: templ.SafeScript(`__templ_elementScriptsLoad_bfb1`),
CallInline: templ.SafeScriptInline(`__templ_elementScriptsLoad_bfb1`),
}
}
func root(page PageKind, _ stats) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
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("<!doctype html><html lang=\"ru\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = head(string(page)).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 = headerNavbar(page).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var11.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 = elementScriptsLoad().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 = footer().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,239 @@
package bootstrap
import "path"
import "strconv"
templ breadcrumbsItem(text, link string, isActive bool) {
<li class={ "breadcrumb-item", templ.KV("active", isActive) }>
if link != "" {
<a
href={ templ.URL(link) }
itemprop="url"
aria-label="breadcrumb"
>{ text }</a>
} else {
<span itemprop="url" itemprop="title">{ text }</span>
}
</li>
}
templ breadcrumNode(params BreadcrumbsParams) {
<nav
class={ "mt-4", breadcrumbSymbol() }
aria-label="breadcrumbs"
itemprop="breadcrumb"
itemtype="https://schema.org/BreadcrumbList"
itemscope
>
<ol class="breadcrumb">
@breadcrumbsItem("Курсы", "/courses", params.ActiveLearningType.Empty())
if !params.ActiveLearningType.Empty() {
@breadcrumbsItem(
params.ActiveLearningType.Name,
path.Join("/", "courses", params.ActiveLearningType.ID),
params.ActiveCourseThematic.Empty(),
)
}
if !params.ActiveCourseThematic.Empty() {
@breadcrumbsItem(
params.ActiveCourseThematic.Name,
path.Join("/", "courses", params.ActiveLearningType.ID, params.ActiveCourseThematic.ID),
true,
)
}
</ol>
</nav>
}
css breadcrumbSymbol() {
--bs-breadcrumb-divider: "/";
}
templ listCoursesSectionHeader(params BreadcrumbsParams) {
<section class="row bc-header">
@breadcrumNode(params)
</section>
}
templ listCoursesSectionFilters(params FilterFormParams) {
<section class="row filters">
<div
class={
templ.KV("visually-hidden", !params.Render),
"p-2",
}
>
<form id="filter-form" class="input-group">
<span class="input-group-text">Filter courses</span>
<select
id="learning-type-filter"
class={ "form-select" }
>
<option value="" selected?={ params.ActiveLearningType.ID=="" }>All</option>
for _, learningType := range params.AvailableLearningTypes {
<option
selected?={ params.ActiveLearningType.ID==learningType.ID }
value={ learningType.ID }
>{ learningType.Name }</option>
}
</select>
<select
id="course-thematic-filter"
class={ "form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0) }
>
<option value="" selected?={ params.ActiveLearningType.ID=="" }>All</option>
for _, courseThematic := range params.AvailableCourseThematics {
<option
selected?={ params.ActiveCourseThematic.ID==courseThematic.ID }
value={ courseThematic.ID }
>{ courseThematic.Name }</option>
}
</select>
<button id="filter-course-thematic" class="btn btn-outline-secondary" type="submit">Go</button>
</form>
</div>
@filterCoursesSelectView(params.Schools)
</section>
}
templ selectOptionFromPairs(emptyName, activeID string, items []NameIDPair) {
<option value="" selected?={ activeID == "" }>
{ emptyName }
</option>
for _, item := range items {
<option
selected?={ activeID==item.ID }
value={ item.ID }
>
{ item.Name }
</option>
}
}
templ filterCoursesSelectView(params CoursesFilterViewParams) {
<div class="filter-content d-flex p-2">
<!-- School list -->
<div class="col-3">
<select name="school" id="schoolSelect" class="form-select" aria-label="school filter">
@selectOptionFromPairs("Pick a school:", params.SelectedSchoolID, params.Schools)
</select>
</div>
<!-- Sort options -->
<div class="col-3 mx-4">
<div class="input-group flex-nowrap">
<select
class="form-select"
id="sortBySelect"
aria-label="Sorting items by requested order"
>
@selectOptionFromPairs("Sort by:", params.OrderBy, params.OrderFields)
</select>
<input
type="radio"
class="btn-check"
name="sortByOrder"
id="sortByOrder"
autocomplete="off"
checked?={ !params.Ascending }
/>
<label class="btn" for="sortByOrder">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-sort-down"
viewBox="0 0 16 16"
>
<path
d="M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"
></path>
</svg>
</label>
</div>
</div>
<div class="col-4"></div>
<div class="col-auto ms-auto">
<div class="btn btn-primary">Promocodes</div>
</div>
</div>
}
templ listCoursesLearning(courses []CourseInfo) {
<div class="block">
<div class="row row-cols-1 row-cols-md-4 g-4">
for _, course := range courses {
@listCoursesCard(course)
}
</div>
</div>
}
templ listCoursesLearningLegacy(containers []CategoryContainer) {
for _, container := range containers {
<section class="row first-class-group g-4">
<h1 class="title">{ container.Name }</h1>
for _, subcategory := range container.Subcategories {
@listCoursesThematicRowLegacy(container.ID, subcategory)
}
</section>
}
}
templ listCoursesThematicRowLegacy(categoryID string, subcategory SubcategoryContainer) {
<div class="block second-class-group">
<h3 class="title">
<a href={ templ.SafeURL("/courses/" + categoryID + "/" + subcategory.ID) } class="text-decoration-none">
{ subcategory.Name }
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-up-right-square" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm5.854 8.803a.5.5 0 1 1-.708-.707L9.243 6H6.475a.5.5 0 1 1 0-1h3.975a.5.5 0 0 1 .5.5v3.975a.5.5 0 1 1-1 0V6.707z"></path>
</svg>
</a>
</h3>
<p class="visually-hidden">В категогрии { 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)
}
</div>
</div>
}
css myImg() {
min-height: 19rem;
min-width: 19rem;
}
css cardTextSize() {
min-height: 12rem;
}
templ listCoursesCard(info CourseInfo) {
// <div class="col-12 col-md-6 col-lg-3">
<div class="col" id={ info.ID }>
<div class="card h-100">
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128") } alt="Course picture" class={ "card-img-top" }/>
<div class={ "card-body", cardTextSize(), "row" }>
<h5 class="card-title">{ info.Name }</h5>
<div class="input-group d-flex align-self-end">
<a
href={ templ.URL(info.OriginLink) }
class="btn text btn-outline-primary flex-grow-1"
>Go!</a>
<span class="input-group-text justify-content-end flex-fill">
{ strconv.Itoa(info.FullPrice) } rub.
</span>
</div>
</div>
</div>
</div>
}
templ ListCourses(pageType PageKind, s stats, params ListCoursesParams) {
@root(pageType, s) {
@listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams)
@listCoursesSectionFilters(params.FilterForm)
@listCoursesLearning(params.Courses)
@pagination(params.Pagination)
}
}

View File

@ -0,0 +1,27 @@
package bootstrap
templ listCoursesByCourseThematic(params ListCoursesParams) {
<div class="container">
<h2>Здесь вы можете найти интересующие вас курсы
по теме { params.FilterForm.ActiveLearningType.Name }:
</h2>
<ul class="list-group">
for _, courseThematic := range params.FilterForm.AvailableCourseThematics {
<li class="list-group-item">
<a href={templ.SafeURL("/courses/" + params.FilterForm.ActiveLearningType.ID + "/" + courseThematic.ID)}>
{courseThematic.Name}
</a>
</li>
}
</ul>
</div>
}
templ ListCourseThematics(pageType PageKind, s stats, params ListCoursesParams) {
@root(pageType, s) {
@listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams)
@listCoursesSectionFilters(params.FilterForm)
@listCoursesByCourseThematic(params)
}
}

View File

@ -0,0 +1,135 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
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"
func listCoursesByCourseThematic(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_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=\"container\"><h2>Здесь вы можете найти интересующие вас курсы по теме ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(params.FilterForm.ActiveLearningType.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list_course_thematics.templ`, Line: 6, Col: 59}
}
_, 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(":</h2><ul class=\"list-group\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, courseThematic := range params.FilterForm.AvailableCourseThematics {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"list-group-item\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL = templ.SafeURL("/courses/" + params.FilterForm.ActiveLearningType.ID + "/" + courseThematic.ID)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
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(courseThematic.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list_course_thematics.templ`, Line: 13, Col: 24}
}
_, 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("</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></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 ListCourseThematics(pageType PageKind, 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_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var6 := 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.FilterForm.BreadcrumbsParams).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 = listCoursesSectionFilters(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 = listCoursesByCourseThematic(params).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_Var6), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,949 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
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 "strings"
import "path"
import "strconv"
func breadcrumbsItem(text, link string, isActive bool) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
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)
var templ_7745c5c3_Var2 = []any{"breadcrumb-item", templ.KV("active", isActive)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
}
_, 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("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if link != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = templ.URL(link)
_, 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("\" itemprop=\"url\" aria-label=\"breadcrumb\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 13, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span itemprop=\"url\" itemprop=\"title\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 15, Col: 47}
}
_, 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("</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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 breadcrumNode(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_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var8 = []any{"mt-4", breadcrumbSymbol()}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-label=\"breadcrumbs\" itemprop=\"breadcrumb\" itemtype=\"https://schema.org/BreadcrumbList\" itemscope><ol class=\"breadcrumb\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = breadcrumbsItem("Курсы", "/courses", params.ActiveLearningType.Empty()).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !params.ActiveLearningType.Empty() {
templ_7745c5c3_Err = breadcrumbsItem(
params.ActiveLearningType.Name,
path.Join("/", "courses", params.ActiveLearningType.ID),
params.ActiveCourseThematic.Empty(),
).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if !params.ActiveCourseThematic.Empty() {
templ_7745c5c3_Err = breadcrumbsItem(
params.ActiveCourseThematic.Name,
path.Join("/", "courses", params.ActiveLearningType.ID, params.ActiveCourseThematic.ID),
true,
).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ol></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 breadcrumbSymbol() templ.CSSClass {
var templ_7745c5c3_CSSBuilder strings.Builder
templ_7745c5c3_CSSBuilder.WriteString(`--bs-breadcrumb-divider:"/";`)
templ_7745c5c3_CSSID := templ.CSSID(`breadcrumbSymbol`, templ_7745c5c3_CSSBuilder.String())
return templ.ComponentCSSClass{
ID: templ_7745c5c3_CSSID,
Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`),
}
}
func listCoursesSectionHeader(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_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"row bc-header\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = breadcrumNode(params).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func listCoursesSectionFilters(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_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("<section class=\"row filters\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 = []any{
templ.KV("visually-hidden", !params.Render),
"p-2",
}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
}
_, 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("\"><form id=\"filter-form\" class=\"input-group\"><span class=\"input-group-text\">Filter courses</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 = []any{"form-select"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<select id=\"learning-type-filter\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option value=\"\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if 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(">All</option> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, learningType := range params.AvailableLearningTypes {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if params.ActiveLearningType.ID == learningType.ID {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(learningType.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 76, Col: 30}
}
_, 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("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, 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: 77, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
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> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 = []any{"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<select id=\"course-thematic-filter\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var18).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option value=\"\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if 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(">All</option> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, courseThematic := range params.AvailableCourseThematics {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if params.ActiveCourseThematic.ID == courseThematic.ID {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 88, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, 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: 89, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> <button id=\"filter-course-thematic\" class=\"btn btn-outline-secondary\" type=\"submit\">Go</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = filterCoursesSelectView(params.Schools).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func selectOptionFromPairs(emptyName, activeID string, items []NameIDPair) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
if templ_7745c5c3_Var22 == nil {
templ_7745c5c3_Var22 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option value=\"\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if activeID == "" {
_, 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_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(emptyName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 101, Col: 13}
}
_, 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("</option> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range items {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if activeID == item.ID {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(item.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 106, Col: 18}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
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_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 108, Col: 14}
}
_, 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("</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func filterCoursesSelectView(params CoursesFilterViewParams) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
if templ_7745c5c3_Var26 == nil {
templ_7745c5c3_Var26 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"filter-content d-flex p-2\"><!-- School list --><div class=\"col-3\"><select name=\"school\" id=\"schoolSelect\" class=\"form-select\" aria-label=\"school filter\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = selectOptionFromPairs("Pick a school:", params.SelectedSchoolID, params.Schools).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select></div><!-- Sort options --><div class=\"col-3 mx-4\"><div class=\"input-group flex-nowrap\"><select class=\"form-select\" id=\"sortBySelect\" aria-label=\"Sorting items by requested order\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = selectOptionFromPairs("Sort by:", params.OrderBy, params.OrderFields).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> <input type=\"radio\" class=\"btn-check\" name=\"sortByOrder\" id=\"sortByOrder\" autocomplete=\"off\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !params.Ascending {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" checked")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("> <label class=\"btn\" for=\"sortByOrder\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-sort-down\" viewBox=\"0 0 16 16\"><path d=\"M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z\"></path></svg></label></div></div><div class=\"col-4\"></div><div class=\"col-auto ms-auto\"><div class=\"btn btn-primary\">Promocodes</div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func listCoursesLearning(courses []CourseInfo) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var27 := templ.GetChildren(ctx)
if templ_7745c5c3_Var27 == nil {
templ_7745c5c3_Var27 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block\"><div class=\"row row-cols-1 row-cols-md-4 g-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, course := range courses {
templ_7745c5c3_Err = listCoursesCard(course).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func listCoursesLearningLegacy(containers []CategoryContainer) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
if templ_7745c5c3_Var28 == nil {
templ_7745c5c3_Var28 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
for _, container := range containers {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"row first-class-group g-4\"><h1 class=\"title\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, 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: 175, Col: 37}
}
_, 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("</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, subcategory := range container.Subcategories {
templ_7745c5c3_Err = listCoursesThematicRowLegacy(container.ID, subcategory).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func listCoursesThematicRowLegacy(categoryID string, subcategory SubcategoryContainer) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var30 := templ.GetChildren(ctx)
if templ_7745c5c3_Var30 == nil {
templ_7745c5c3_Var30 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block second-class-group\"><h3 class=\"title\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 templ.SafeURL = templ.SafeURL("/courses/" + categoryID + "/" + subcategory.ID)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var31)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"text-decoration-none\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, 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: 187, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-up-right-square\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm5.854 8.803a.5.5 0 1 1-.708-.707L9.243 6H6.475a.5.5 0 1 1 0-1h3.975a.5.5 0 0 1 .5.5v3.975a.5.5 0 1 1-1 0V6.707z\"></path></svg></a></h3><p class=\"visually-hidden\">В категогрии ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 193, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
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_Var34 string
templ_7745c5c3_Var34, 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: 193, Col: 122}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" курсов. Раз в неделю мы обновляем информацию о всех курсах.</p><div class=\"row row-cols-1 row-cols-md-4 g-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, info := range subcategory.Courses {
templ_7745c5c3_Err = listCoursesCard(info).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func myImg() templ.CSSClass {
var templ_7745c5c3_CSSBuilder strings.Builder
templ_7745c5c3_CSSBuilder.WriteString(`min-height:19rem;`)
templ_7745c5c3_CSSBuilder.WriteString(`min-width:19rem;`)
templ_7745c5c3_CSSID := templ.CSSID(`myImg`, templ_7745c5c3_CSSBuilder.String())
return templ.ComponentCSSClass{
ID: templ_7745c5c3_CSSID,
Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`),
}
}
func cardTextSize() templ.CSSClass {
var templ_7745c5c3_CSSBuilder strings.Builder
templ_7745c5c3_CSSBuilder.WriteString(`min-height:12rem;`)
templ_7745c5c3_CSSID := templ.CSSID(`cardTextSize`, templ_7745c5c3_CSSBuilder.String())
return templ.ComponentCSSClass{
ID: templ_7745c5c3_CSSID,
Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`),
}
}
func listCoursesCard(info 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_Var35 := templ.GetChildren(ctx)
if templ_7745c5c3_Var35 == nil {
templ_7745c5c3_Var35 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(info.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 213, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><div class=\"card h-100\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var37 = []any{"card-img-top"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var37...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(GetOrFallback(info.ImageLink, "https://placehold.co/128x128"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 215, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" alt=\"Course picture\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var37).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
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_Var40 = []any{"card-body", cardTextSize(), "row"}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var40...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var40).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><h5 class=\"card-title\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, 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: 217, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h5><div class=\"input-group d-flex align-self-end\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var43 templ.SafeURL = templ.URL(info.OriginLink)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var43)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"btn text btn-outline-primary flex-grow-1\">Go!</a> <span class=\"input-group-text justify-content-end flex-fill\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var44 string
templ_7745c5c3_Var44, 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: 224, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" rub.</span></div></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 ListCourses(pageType PageKind, 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_Var45 := templ.GetChildren(ctx)
if templ_7745c5c3_Var45 == nil {
templ_7745c5c3_Var45 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var46 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
templ_7745c5c3_Err = listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams).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 = listCoursesSectionFilters(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 = listCoursesLearning(params.Courses).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 = pagination(params.Pagination).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_Var46), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,96 @@
package bootstrap
import "strconv"
type IndexCourseCategoryItem struct {
ID string
Name string
Description string
ExampleThemes []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>
if len(item.ExampleThemes) > 0 {
<p>В данной категории вы можете найти курсы по темам:</p>
<ul>
for _, exampleItem := range item.ExampleThemes {
<li>
<span class="d-inline-block text-truncate col-8">{ exampleItem }</span>
</li>
}
</ul>
}
<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 mb-4">
<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 Pagination struct {
Page int
TotalPages int
BaseURL string
}
templ pagination(p Pagination) {
if p.Page > 0 && p.TotalPages > 0 {
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class={ "page-item" , templ.KV("disabled", p.Page==1), }>
<a href={ templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page-1)) } class="page-link">Previous</a>
</li>
for i := max(p.Page-2, 1); i < min(p.TotalPages, 10); i++ {
<li
class={ "page-item" , templ.KV("active", p.Page==i), }
>
<a href={ templ.URL(p.BaseURL + "?page=" + strconv.Itoa(i)) } class="page-link">{ strconv.Itoa(i) }</a>
</li>
}
<li class={ "page-item" , templ.KV("disabled", p.Page==p.TotalPages), }>
<a href={ templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page+1)) } class="page-link">Next</a>
</li>
</ul>
</nav>
}
}
type MainPageParams struct {
Breadcrumbs BreadcrumbsParams
Categories []IndexCourseCategoryItem
Pagination Pagination
}
templ MainPage(pageType PageKind, s stats, params MainPageParams) {
@root(pageType, s) {
@listCoursesSectionHeader(params.Breadcrumbs)
@courseCategory(params.Categories)
@pagination(params.Pagination)
}
}

View File

@ -0,0 +1,372 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
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
ExampleThemes []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: 19, 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: 21, 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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(item.ExampleThemes) > 0 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>В данной категории вы можете найти курсы по темам:</p><ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, exampleItem := range item.ExampleThemes {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><span class=\"d-inline-block text-truncate col-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(exampleItem)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 27, Col: 69}
}
_, 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("</span></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"d-flex justify-content-between align-items-center\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL = templ.URL("/courses/" + item.ID)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5)))
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\">Open</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: 37, 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(" items.</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_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container w-75 mb-4\"><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 Pagination struct {
Page int
TotalPages int
BaseURL string
}
func pagination(p Pagination) 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)
if p.Page > 0 && p.TotalPages > 0 {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav aria-label=\"Page navigation\"><ul class=\"pagination justify-content-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 = []any{"page-item", templ.KV("disabled", p.Page == 1)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 1, Col: 0}
}
_, 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("\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page-1))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var11)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"page-link\">Previous</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for i := max(p.Page-2, 1); i < min(p.TotalPages, 10); i++ {
var templ_7745c5c3_Var12 = []any{"page-item", templ.KV("active", p.Page == i)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 1, Col: 0}
}
_, 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 href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(i))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"page-link\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 73, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
var templ_7745c5c3_Var16 = []any{"page-item", templ.KV("disabled", p.Page == p.TotalPages)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var16).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page+1))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var18)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"page-link\">Next</a></li></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
})
}
type MainPageParams struct {
Breadcrumbs BreadcrumbsParams
Categories []IndexCourseCategoryItem
Pagination Pagination
}
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_Var19 := templ.GetChildren(ctx)
if templ_7745c5c3_Var19 == nil {
templ_7745c5c3_Var19 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var20 := 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
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = pagination(params.Pagination).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_Var20), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -1,10 +1,18 @@
package templ package bootstrap
import ( import (
"strconv" "strconv"
"strings" "strings"
) )
type PageKind string
const (
PageIndex PageKind = "main"
PageCourses PageKind = "courses"
PageAbout PageKind = "about"
)
func getCompactedValue(value int) string { func getCompactedValue(value int) string {
var ( var (
myValue float64 myValue float64
@ -37,6 +45,29 @@ type stats struct {
CategoriesCount string CategoriesCount string
} }
type NameIDPair struct {
ID string
Name string
}
type SortingItem struct {
NameIDPair
Ascending bool
}
type SortingView struct {
Items []SortingItem
}
type CoursesFilterViewParams struct {
SelectedSchoolID string
Schools []NameIDPair
OrderBy string
OrderFields []NameIDPair
Ascending bool
}
type Category struct { type Category struct {
ID string ID string
Name string Name string
@ -54,12 +85,10 @@ type BreadcrumbsParams struct {
type FilterFormParams struct { type FilterFormParams struct {
BreadcrumbsParams BreadcrumbsParams
Schools CoursesFilterViewParams
AvailableLearningTypes []Category AvailableLearningTypes []Category
AvailableCourseThematics []Category AvailableCourseThematics []Category
} Render bool
func isEmpty(s string) bool {
return s == ""
} }
type CourseInfo struct { type CourseInfo struct {
@ -74,6 +103,7 @@ type CategoryBaseInfo struct {
ID string ID string
Name string Name string
Description string Description string
Count int
} }
type CategoryContainer struct { type CategoryContainer struct {
@ -90,6 +120,38 @@ type SubcategoryContainer struct {
type ListCoursesParams struct { type ListCoursesParams struct {
FilterForm FilterFormParams FilterForm FilterFormParams
Categories []CategoryContainer Categories []CategoryContainer
Pagination Pagination
Items int
Courses []CourseInfo
}
func GetOrFallback[T comparable](value T, fallback T) T {
var zeroValue T
if value == zeroValue {
return fallback
}
return value
}
type ordered interface {
~int8 | ~int16 | ~int32 | ~int64 | ~int |
~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uint |
~float32 | float64 | ~string
}
func min[T ordered](lhs, rhs T) T {
if lhs < rhs {
return lhs
}
return rhs
}
func max[T ordered](lhs, rhs T) T {
if lhs > rhs {
return lhs
}
return rhs
} }

View File

@ -2,343 +2,405 @@ package http
import ( import (
"encoding/json" "encoding/json"
"fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"sort" "slices"
"strconv" "sync"
"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/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/app/query"
"git.loyso.art/frx/kurious/internal/kurious/domain" "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" "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/codes"
"go.opentelemetry.io/otel/trace"
) )
type courseServer struct { var (
paramsAttr = attribute.Key("params")
webtracer = otel.Tracer("http")
)
type courseTemplServer struct {
app service.Application app service.Application
log *slog.Logger log *slog.Logger
} }
type pagination struct { func makeTemplListCoursesParams(counts map[string]domain.LearningTypeStat, in ...domain.Course) bootstrap.ListCoursesParams {
nextPageToken string coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in))
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))
subcategoriesByCategories := make(map[string]map[string]struct{}, 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))
seenCourses := make(map[string]struct{}, len(in))
var out bootstrap.ListCoursesParams
xslices.ForEach(in, func(c domain.Course) { xslices.ForEach(in, func(c domain.Course) {
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 { if _, ok := subcategoriesByCategories[c.LearningTypeID]; !ok {
subcategoriesByCategories[c.LearningTypeID] = map[string]struct{}{} subcategoriesByCategories[c.LearningTypeID] = map[string]struct{}{}
} }
subcategoriesByCategories[c.LearningTypeID][c.ThematicID] = struct{}{} subcategoriesByCategories[c.LearningTypeID][c.ThematicID] = struct{}{}
if _, ok := categoryByID[c.LearningTypeID]; !ok { if _, ok := categoryByID[c.LearningTypeID]; !ok {
categoryByID[c.LearningTypeID] = baseInfo{ categoryByID[c.LearningTypeID] = bootstrap.CategoryBaseInfo{
ID: c.LearningTypeID, ID: c.LearningTypeID,
Name: c.LearningType, Name: c.LearningType,
} }
} }
if _, ok := categoryByID[c.ThematicID]; !ok { if _, ok := categoryByID[c.ThematicID]; !ok {
categoryByID[c.ThematicID] = baseInfo{ categoryByID[c.ThematicID] = bootstrap.CategoryBaseInfo{
ID: c.ThematicID, ID: c.ThematicID,
Name: c.Thematic, Name: c.Thematic,
Count: counts[c.LearningTypeID].CourseThematic[c.ThematicID],
} }
} }
if _, ok := seenCourses[c.ExternalID.Value()]; ok && c.ExternalID.Valid() {
return
}
out.Courses = append(out.Courses, courseInfo)
seenCourses[c.ExternalID.Value()] = struct{}{}
}) })
var out listCoursesTemplateParams for categoryID, subcategoriesID := range subcategoriesByCategories {
for category, subcategoryMap := range subcategoriesByCategories { outCategory := bootstrap.CategoryContainer{
outCategory := categoryInfo{ CategoryBaseInfo: categoryByID[categoryID],
baseInfo: categoryByID[category],
} }
for subcategory := range subcategoryMap { for subcategoryID := range subcategoriesID {
outSubCategory := subcategoryInfo{ outSubcategory := bootstrap.SubcategoryContainer{
baseInfo: categoryByID[subcategory], CategoryBaseInfo: categoryByID[subcategoryID],
Courses: coursesBySubcategory[subcategory], 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) 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 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() ctx := r.Context()
params, err := parseListCoursesParams(r) var span trace.Span
ctx, span = webtracer.Start(ctx, "http_server.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") { if handleError(ctx, err, w, c.log, "unable to parse list courses params") {
return return
} }
result, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{ jsonParams, _ := json.Marshal(pathParams)
CourseThematic: params.courseThematic, span.SetAttributes(paramsAttr.String(string(jsonParams)))
LearningType: params.learningType,
Limit: params.perPage, var offset int
NextPageToken: params.nextPageToken, if pathParams.Page > 0 {
offset = (pathParams.Page - 1) * pathParams.PerPage
}
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
CourseThematic: pathParams.CourseThematic,
LearningType: pathParams.LearningType,
OrganizationID: pathParams.School,
OrderBy: orderByListing.getField(pathParams.OrderBy),
Ascending: pathParams.Ascending,
Limit: pathParams.PerPage,
NextPageToken: pathParams.NextPageToken,
Offset: offset,
}) })
if handleError(ctx, err, w, c.log, "unable to list courses") { if handleError(ctx, err, w, c.log, "unable to list courses") {
return return
} }
courses := result.Courses statsresult, err := c.app.Queries.ListCourseStatistics.Handle(ctx, query.ListCoursesStats{
templateCourses := mapDomainCourseToTemplate(courses...) LearningTypeID: pathParams.LearningType,
templateCourses.NextPageToken = result.NextPageToken CourseThematicsID: pathParams.CourseThematic,
OrganizationID: pathParams.School,
})
if handleError(ctx, err, w, c.log, "unable to load stats") {
return
}
learningTypeList, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{}) params := makeTemplListCoursesParams(statsresult.StatsByLearningType, listCoursesResult.Courses...)
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
if handleError(ctx, err, w, c.log, "unable to list learning types") { if handleError(ctx, err, w, c.log, "unable to list learning types") {
return return
} }
templateCourses.AvailableLearningTypes = xslices.Map(learningTypeList.LearningTypes, func(in query.LearningType) IDNamePair { params.FilterForm.AvailableLearningTypes = xslices.Map(learningTypeResult.LearningTypes, func(in query.LearningType) bootstrap.Category {
if in.ID == params.learningType { outcategory := bootstrap.Category{
templateCourses.LearningTypeName = in.Name
}
return IDNamePair{
ID: in.ID, ID: in.ID,
Name: in.Name, Name: in.Name,
IsActive: in.ID == params.learningType,
} }
if in.ID == pathParams.LearningType {
params.FilterForm.ActiveLearningType = outcategory
}
return outcategory
}) })
templateCourses.ActiveLearningType = params.learningType if pathParams.LearningType != "" {
templateCourses.ActiveCourseThematic = params.courseThematic
if params.learningType != "" {
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{ 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") { if handleError(ctx, err, w, c.log, "unable to list course thematics") {
return return
} }
templateCourses.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) IDNamePair { params.FilterForm.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) bootstrap.Category {
if in.ID == params.courseThematic { outcategory := bootstrap.Category{
templateCourses.CourseThematicName = in.Name
}
return IDNamePair{
ID: in.ID, ID: in.ID,
Name: in.Name, Name: in.Name,
IsActive: in.ID == params.courseThematic,
} }
if pathParams.CourseThematic == in.ID {
params.FilterForm.ActiveCourseThematic = outcategory
}
return outcategory
}) })
} }
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "courses", templateCourses) organizaions, err := c.app.Queries.ListOrganizationsStats.Handle(ctx, query.ListOrganizationsStats{
if handleError(ctx, err, w, c.log, "unable to execute template") { LearningTypeID: pathParams.LearningType,
CourseThematicID: pathParams.CourseThematic,
})
if handleError(ctx, err, w, c.log, "unable to list organizations") {
return return
} }
organizationStatSortFunc := func(lhs, rhs domain.OrganizationStat) int {
if lhs.CoursesCount > rhs.CoursesCount {
return -1
} else if lhs.CoursesCount < rhs.CoursesCount {
return 1
} }
func (c courseServer) Get(w http.ResponseWriter, r *http.Request) { if lhs.ID > rhs.ID {
return 1
}
return -1
}
slices.SortFunc(organizaions, organizationStatSortFunc)
schools := xslices.Map(organizaions, func(in domain.OrganizationStat) bootstrap.NameIDPair {
return bootstrap.NameIDPair{
ID: in.ID,
Name: fmt.Sprintf("%s (count: %d)", in.Name, in.CoursesCount),
}
})
params = bootstrap.ListCoursesParams{
FilterForm: bootstrap.FilterFormParams{
Render: true,
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
ActiveLearningType: params.FilterForm.ActiveLearningType,
ActiveCourseThematic: params.FilterForm.ActiveCourseThematic,
},
AvailableLearningTypes: params.FilterForm.AvailableLearningTypes,
AvailableCourseThematics: params.FilterForm.AvailableCourseThematics,
Schools: bootstrap.CoursesFilterViewParams{
SelectedSchoolID: pathParams.School,
Schools: schools,
Ascending: pathParams.Ascending,
OrderBy: pathParams.OrderBy,
OrderFields: orderByListing.asNameIDPair(),
},
},
Courses: params.Courses,
Categories: params.Categories,
Pagination: bootstrap.Pagination{
Page: pathParams.Page,
TotalPages: listCoursesResult.Count / pathParams.PerPage,
BaseURL: r.URL.Path,
},
}
c.log.DebugContext(
ctx, "params rendered",
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
slog.Int("items", len(listCoursesResult.Courses)),
slog.Int("page", params.Pagination.Page),
slog.Int("total_pages", params.Pagination.TotalPages),
)
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.CategoryContainer) int {
if lhs.Count > rhs.Count {
return 1
} else if lhs.Count < rhs.Count {
return -1
} else {
return 0
}
})
span.AddEvent("starting to render")
if pathParams.CourseThematic == "" {
params.FilterForm.Render = true
err = bootstrap.ListCourseThematics(bootstrap.PageCourses, stats, params).Render(ctx, w)
} else {
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
}
span.SetStatus(codes.Ok, "request completed")
}
func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
id := mux.Vars(r)["course_id"] var span trace.Span
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{ ctx, span = webtracer.Start(ctx, "http_server.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)
})
for learningTypeInfo, courses := range coursesByLearningType {
category := bootstrap.IndexCourseCategoryItem{
ID: learningTypeInfo.ID,
Name: learningTypeInfo.Name,
Count: len(courses),
}
xslices.Shuffle(courses)
if len(courses) > 3 {
courses = courses[:3]
}
names := xslices.Map(courses, func(in domain.Course) string {
return in.Name
})
category.ExampleThemes = names
params.Categories = append(params.Categories, category)
}
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.IndexCourseCategoryItem) int {
if lhs.Count < rhs.Count {
return 1
} else if lhs.Count > rhs.Count {
return -1
}
return 0
})
span.AddEvent("starting to render")
err = bootstrap.MainPage(bootstrap.PageIndex, stats, params).Render(ctx, w)
span.AddEvent("render finished")
if handleError(ctx, err, w, c.log, "rendeting template") {
return
}
span.SetStatus(codes.Ok, "request completed")
}
var orderByListing = newOrderableContainer(
newOrderableUnit("pr", "Price", "full_price"),
newOrderableUnit("na", "Name", "name"),
newOrderableUnit("di", "Discount", "discount"),
newOrderableUnit("du", "Duration", "duration"),
newOrderableUnit("st", "Starts At", "starts_at"),
)
type orderableUnit struct {
ID string
Name string
Field string
}
func newOrderableUnit(id, name, field string) orderableUnit {
return orderableUnit{
ID: id, ID: id,
}) Name: name,
if handleError(ctx, err, w, c.log, "unable to get course") { Field: field,
return
}
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")
} }
} }
func (c courseServer) GetShort(w http.ResponseWriter, r *http.Request) { type orderableContainer struct {
ctx := r.Context() nameByID map[string]string
fieldByID map[string]string
cachedNameIDPair []bootstrap.NameIDPair
makeCache sync.Once
}
id := mux.Vars(r)["course_id"] func (c *orderableContainer) asNameIDPair() []bootstrap.NameIDPair {
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{ c.makeCache.Do(func() {
c.cachedNameIDPair = make([]bootstrap.NameIDPair, 0, len(c.nameByID))
for id, name := range c.nameByID {
c.cachedNameIDPair = append(c.cachedNameIDPair, bootstrap.NameIDPair{
ID: id, ID: id,
Name: name,
}) })
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) RenderEditDescription(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := mux.Vars(r)["course_id"]
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
ID: id,
}) })
if handleError(ctx, err, w, c.log, "unable to get course") {
return return c.cachedNameIDPair
} }
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "edit_description", course) func (c *orderableContainer) getField(id string) string {
if handleError(ctx, err, w, c.log, "unable to execute template") { return c.fieldByID[id]
return
}
} }
func (c courseServer) UpdateCourseDescription(w http.ResponseWriter, r *http.Request) { func newOrderableContainer(units ...orderableUnit) *orderableContainer {
type requestModel struct { nameByID := make(map[string]string, len(units))
ID string `json:"-"` fieldByID := make(map[string]string, len(units))
Text string `json:"description"`
}
ctx := r.Context() xslices.ForEach(units, func(u orderableUnit) {
nameByID[u.ID] = u.Name
var req requestModel fieldByID[u.ID] = u.Field
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{ return &orderableContainer{
ID: req.ID, nameByID: nameByID,
}) fieldByID: fieldByID,
if handleError(ctx, err, w, c.log, "unable to get course") {
return
}
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "course_info", course)
if handleError(ctx, err, w, c.log, "unable to execute template") {
return
} }
} }
func (c courseServer) UdpateDescription(w http.ResponseWriter, r *http.Request) {
type requestModel struct {
ID string `json:"id"`
Text string `json:"text"`
}
ctx := r.Context()
var req requestModel
err := json.NewDecoder(r.Body).Decode(&req)
if handleError(ctx, err, w, c.log, "unable to read body") {
return
}
err = c.app.Commands.UpdateCourseDescription.Handle(ctx, command.UpdateCourseDescription{
ID: req.ID,
Description: req.Text,
})
if handleError(ctx, err, w, c.log, "unable to update course description") {
return
}
w.WriteHeader(http.StatusOK)
}

View File

@ -1,60 +0,0 @@
package http
import (
"strconv"
"strings"
"testing"
"time"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
var courses = func() []domain.Course {
out := make([]domain.Course, 0)
out = append(out, makeBatchCourses("prog", []string{"go", "rust"}, 4)...)
out = append(out, makeBatchCourses("front", []string{"js", "html"}, 4)...)
return out
}()
func makeBatchCourses(lt string, cts []string, num int) []domain.Course {
out := make([]domain.Course, 0, len(cts)*num)
for _, ct := range cts {
for i := 0; i < num; i++ {
name := strings.Join([]string{
lt, ct,
strconv.Itoa(i),
}, ".")
out = append(out, makeCourse(lt, ct, name))
}
}
return out
}
func makeCourse(lt, ct, name string) domain.Course {
return domain.Course{
LearningType: lt,
Thematic: ct,
Name: name,
ID: lt + ct + name,
FullPrice: 123,
Duration: time.Second * 100,
StartsAt: time.Now(),
}
}
func TestRenderTemplate(t *testing.T) {
t.SkipNow()
result := mapDomainCourseToTemplate(courses...)
t.Logf("%#v", result)
var out strings.Builder
err := listTemplateParsed.ExecuteTemplate(&out, "courses", result)
if err != nil {
t.Fatalf("executing: %v", err)
}
t.Log(out.String())
t.Fail()
}

View File

@ -1,139 +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"
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(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,
}
}
})
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
}
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.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.BreadcrumbsParams.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, "unab;e 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
})
}
err = xtempl.ListCourses(stats, params).Render(ctx, w)
if handleError(ctx, err, w, c.log, "unable to render list courses") {
return
}
}

View File

@ -1,192 +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/templates/"
func must[T any](t T, err error) T {
if err != nil {
panic(err.Error())
}
return t
}
func scanFiles() []string {
entries := xslices.Map(
must(os.ReadDir(baseTemplatePath)),
func(v fs.DirEntry) string {
return path.Join(baseTemplatePath, v.Name())
},
)
return entries
}
func getCoreTemplate(ctx context.Context, log *slog.Logger) *template.Template {
filenames := scanFiles()
out, err := template.New("courses").ParseFiles(filenames...)
if err != nil {
xcontext.LogWithWarnError(ctx, log, err, "unable to parse template")
return listTemplateParsed
}
return out
}
var listTemplateParsed = template.Must(
template.New("courses").
Parse(listTemplate),
)
const listTemplate = `{{define "courses"}}
<!DOCTYPE html>
<html>
<head>
<title>Courses</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
header {
background-color: #333;
color: white;
padding: 10px;
}
header h1 {
margin: 0;
}
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
}
nav li {
display: inline;
margin-right: 10px;
}
nav a {
color: white;
text-decoration: none;
}
h1, h2, h3 {
margin-top: 0;
}
p {
margin-bottom: 10px;
}
.main-course {
background-color: #3B4252;
color: #E5E9F0;
text-align: center;
}
.sub-course {
background-color: #4C566A;
color: #ECEFF4;
}
.course-plate {
background-color: #f2f2f2;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
.course-plate a {
color: #333;
text-decoration: none;
}
.course-plate a:hover {
text-decoration: underline;
}
.editable-text {
cursor: pointer;
}
.editable-text.editing {
border: 1px solid #000;
padding: 5px;
width: 100%;
}
</style>
</head>
<body>
<header>
<h1>Courses</h1>
<nav>
<ul>
<li><a href="/">Main page</a></li>
<li><a href="/about">About us</a></li>
<li><a href="/help">Help</a></li>
</ul>
</nav>
</header>
{{range $category := .Categories}}
<h2 class="main-course">Category {{$category.Name}}</h2>
<p> Course Description: {{$category.Description}}</p>
{{range $subcategory := $category.Subcategories}}
<div>
<h2 class="sub-course"> Subcategory: {{$subcategory.Name}}</h2>
<p>Subcategory Description: {{$subcategory.Description}}</p>
{{range $course := $subcategory.Courses}}
<div class="course-plate">
<h3><a href="/courses/{{$course.ID}}">{{$course.Name}}</a></h3>
<p>Description: <div id="editable-text-{{$course.ID}}" class="editable-text" contenteditable=false>{{or $course.Description "..."}}</div></p>
<p>Full price: {{$course.FullPrice}}</p>
<p>Discount: {{$course.Discount}}</p>
<p>Thematic: {{$course.Thematic}}</p>
<p>Learning type: {{$course.LearningType}}</p>
<p>Duration: {{$course.Duration}}</p>
<p>Starts at: {{$course.StartsAt}}</p>
</div>
</div>
{{end}}
{{end}}
{{end}}
<button onclick="window.location.href='/courses/?next={{.NextPageToken}}'">Next Page</button>
<script>
const editableTexts = document.querySelectorAll('.editable-text');
let isEditing = false;
editableTexts.forEach(function(editableText) {
editableText.addEventListener('click', function() {
if (!isEditing) {
editableText.contentEditable = 'true';
editableText.className += ' editing';
isEditing = true;
}
});
editableText.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
const text = editableText.innerText;
const id = editableText.id.replace('editable-text-', ''); // Extract the ID from the element's ID
// Send a POST request with JSON data
const xhr = new XMLHttpRequest();
xhr.open('POST', '/updatedesc', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ text, id }));
editableText.contentEditable = 'false';
editableText.className = 'editable-text';
isEditing = false;
}
});
});
</script>
</body>
</html>
{{end}}`

View File

@ -5,10 +5,16 @@ import (
stderrors "errors" stderrors "errors"
"log/slog" "log/slog"
"net/http" "net/http"
"strconv"
"git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/common/xcontext" "git.loyso.art/frx/kurious/internal/common/xcontext"
"git.loyso.art/frx/kurious/internal/kurious/service" "git.loyso.art/frx/kurious/internal/kurious/service"
"git.loyso.art/frx/kurious/pkg/xdefault"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
) )
type Server struct { type Server struct {
@ -23,11 +29,7 @@ func NewServer(app service.Application, log *slog.Logger) Server {
} }
} }
func (s Server) Courses() courseServer { func (s Server) Courses() courseTemplServer {
return courseServer(s)
}
func (s Server) CoursesByTempl() courseTemplServer {
return courseTemplServer(s) return courseTemplServer(s)
} }
@ -36,6 +38,10 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
return false return false
} }
span := trace.SpanFromContext(ctx)
span.RecordError(err)
span.SetStatus(codes.Error, "error during handling request")
var errorString string var errorString string
var code int var code int
valErr := new(errors.ValidationError) valErr := new(errors.ValidationError)
@ -57,3 +63,79 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
return true return true
} }
type pagination struct {
NextPageToken string
PerPage int
Page int
}
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
query := r.URL.Query()
if query.Has("next") && query.Has("page") {
return out, errors.NewValidationError("next", `could not be set together with "page"`)
}
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 = 20
}
if query.Has("page") {
out.Page, err = strconv.Atoi(query.Get("page"))
if err != nil {
return out, errors.NewValidationError("page", "bad per_page value")
}
} else if !query.Has("next") {
out.Page = 1
}
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]
out.School = r.URL.Query().Get("school_id")
out.OrderBy = xdefault.WithFallback(r.URL.Query().Get("order_by"), "price")
if r.URL.Query().Has("asc") {
out.Ascending, _ = strconv.ParseBool(r.URL.Query().Get("asc"))
}
return out, nil
}
type listCoursesParams struct {
pagination
CourseThematic string
LearningType string
School string
OrderBy string
Ascending bool
}
type IDNamePair struct {
ID string
Name string
IsActive bool
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,11 +2,13 @@ package service
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
"git.loyso.art/frx/kurious/internal/common/config" "git.loyso.art/frx/kurious/internal/common/config"
"git.loyso.art/frx/kurious/internal/common/xcontext"
"git.loyso.art/frx/kurious/internal/kurious/adapters" "git.loyso.art/frx/kurious/internal/kurious/adapters"
"git.loyso.art/frx/kurious/internal/kurious/app" "git.loyso.art/frx/kurious/internal/kurious/app"
"git.loyso.art/frx/kurious/internal/kurious/app/command" "git.loyso.art/frx/kurious/internal/kurious/app/command"
@ -14,9 +16,19 @@ import (
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
) )
type RepositoryEngine uint8
const (
RepositoryEngineUnknown RepositoryEngine = iota
RepositoryEngineYDB
RepositoryEngineSqlite
)
type ApplicationConfig struct { type ApplicationConfig struct {
LogConfig config.Log LogConfig config.Log
YDB config.YDB YDB config.YDB
Sqlite config.Sqlite
Engine RepositoryEngine
} }
type Application struct { type Application struct {
@ -26,14 +38,32 @@ type Application struct {
closers []io.Closer closers []io.Closer
} }
func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.CourseMapper) (Application, error) { func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.CourseMapper) (out Application, err error) {
log := config.NewSLogger(cfg.LogConfig) log := config.NewSLogger(cfg.LogConfig)
ydbConnection, err := adapters.NewYDBConnection(ctx, cfg.YDB, log.With(slog.String("db", "ydb")))
var repoCloser io.Closer
var courseadapter domain.CourseRepository
var organizationrepo domain.OrganizationRepository
switch cfg.Engine {
case RepositoryEngineSqlite:
sqliteConnection, err := adapters.NewSqliteConnection(ctx, cfg.Sqlite, log.With(slog.String("db", "sqlite")))
if err != nil { if err != nil {
return Application{}, fmt.Errorf("making ydb connection: %w", err) return Application{}, fmt.Errorf("making sqlite connection: %w", err)
} }
courseadapter := ydbConnection.CourseRepository() courseadapter = sqliteConnection.CourseRepository()
organizationrepo = sqliteConnection.Organization()
repoCloser = sqliteConnection
case RepositoryEngineYDB:
return Application{}, errors.New("ydb is no longer supported")
default:
return Application{}, errors.New("unable to decide which db engine to use")
}
err = mapper.CollectCounts(ctx, courseadapter)
if err != nil {
xcontext.LogWithWarnError(ctx, log, err, "unable to properly collect counts")
}
application := app.Application{ application := app.Application{
Commands: app.Commands{ Commands: app.Commands{
@ -41,17 +71,24 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.Co
InsertCourse: command.NewCreateCourseHandler(courseadapter, log), InsertCourse: command.NewCreateCourseHandler(courseadapter, log),
DeleteCourse: command.NewDeleteCourseHandler(courseadapter, log), DeleteCourse: command.NewDeleteCourseHandler(courseadapter, log),
UpdateCourseDescription: command.NewUpdateCourseDescriptionHandler(courseadapter, log), UpdateCourseDescription: command.NewUpdateCourseDescriptionHandler(courseadapter, log),
InsertOrganization: command.NewCreateOrganizationHandler(organizationrepo, log),
}, },
Queries: app.Queries{ Queries: app.Queries{
ListCourses: query.NewListCourseHandler(courseadapter, mapper, log), ListCourses: query.NewListCourseHandler(courseadapter, mapper, log),
ListLearningTypes: query.NewListLearningTypesHandler(courseadapter, mapper, log), ListLearningTypes: query.NewListLearningTypesHandler(courseadapter, mapper, log),
ListCourseThematics: query.NewListCourseThematicsHandler(courseadapter, mapper, log), ListCourseThematics: query.NewListCourseThematicsHandler(courseadapter, mapper, log),
ListCourseStatistics: query.NewListCoursesStatsHandler(mapper, courseadapter, log),
GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log), GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log),
ListOrganzations: query.NewListOrganizationsHandler(organizationrepo, log),
ListOrganizationsStats: query.NewListOrganizationsStatsHandler(organizationrepo, log),
GetOrganization: query.NewGetOrganizationHandler(organizationrepo, log),
}, },
} }
out := Application{Application: application} out = Application{Application: application}
out.closers = append(out.closers, ydbConnection) out.closers = append(out.closers, repoCloser)
out.log = log out.log = log
return out, nil return out, nil

View File

@ -0,0 +1,24 @@
CREATE TABLE courses (
id TEXT PRIMARY KEY,
external_id TEXT,
source_type TEXT NOT NULL,
source_name TEXT,
course_thematic TEXT NOT NULL,
learning_type TEXT NOT NULL,
organization_id TEXT NOT NULL,
origin_link TEXT NOT NULL,
image_link TEXT,
name TEXT NOT NULL,
description TEXT NOT NULL,
full_price REAL NOT NULL,
discount REAL NOT NULL,
duration INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
starts_at DATETIME,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME
);
CREATE INDEX idx_course_thematic ON courses (course_thematic);
CREATE INDEX idx_learning_type ON courses (learning_type);
CREATE INDEX idx_organization_id ON courses (organization_id);

View File

@ -0,0 +1,19 @@
CREATE TABLE learning_categories (
id TEXT PRIMARY KEY,
logo TEXT NULL,
courses_count INT NOT NULL DEFAULT 0
);
create table organizations (
id TEXT PRIMARY KEY,
external_id TEXT NULL,
alias TEXT NOT NULL,
name TEXT NOT NULL,
site TEXT NOT NULL,
logo TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME NULL
);
CREATE INDEX idx_organization_external_id ON organizations (external_id);

View File

@ -0,0 +1,18 @@
package sqlite
import (
"embed"
"io/fs"
)
//go:embed *.sql
var migrations embed.FS
func getMigrationEntries() ([]fs.DirEntry, error) {
entries, err := fs.ReadDir(migrations, ".")
if err != nil {
return nil, err
}
return entries, nil
}

View File

@ -0,0 +1,240 @@
package sqlite
import (
"cmp"
"context"
"database/sql"
"errors"
"fmt"
"io/fs"
"log/slog"
"path"
"slices"
"strconv"
"strings"
"time"
"git.loyso.art/frx/kurious/internal/common/xcontext"
)
type migrationUnit struct {
num int
name string
path string
}
func (u migrationUnit) apply(ctx context.Context, tx *sql.Tx) error {
content, err := fs.ReadFile(migrations, u.path)
if err != nil {
return fmt.Errorf("reading file: %w", err)
}
query := string(content)
_, err = tx.ExecContext(ctx, query)
if err != nil {
return fmt.Errorf("executing query: %w", err)
}
return nil
}
func sortMigrationUnit(lhs, rhs migrationUnit) int {
if lhs.num < rhs.num {
return -1
} else if lhs.num > rhs.num {
return 1
} else {
return 0
}
}
func RunMigrations(ctx context.Context, db *sql.DB, log *slog.Logger) error {
items, err := getMigrationEntries()
if err != nil {
return fmt.Errorf("reading directory: %w", err)
}
units := make([]migrationUnit, 0, len(items))
for _, item := range items {
if item.IsDir() {
continue
}
itemName := item.Name()
splitted := strings.SplitN(itemName, "_", 2)
if len(splitted) != 2 {
return fmt.Errorf("bad number of parts, expected 2, got %d", len(splitted))
}
splittedNum, err := strconv.Atoi(splitted[0])
if err != nil {
return fmt.Errorf("parsing migration number: %w", err)
}
if splittedNum < 1 {
return fmt.Errorf("migration number expected to be greater than 0, but got %d", splittedNum)
}
unit := migrationUnit{
num: splittedNum,
name: strings.TrimSuffix(splitted[1], ".sql"),
path: path.Join(itemName),
}
xcontext.LogDebug(ctx, log, "found migration unit", slog.Any("unit", unit))
units = append(units, unit)
}
slices.SortFunc(units, sortMigrationUnit)
mr := &metaRepository{
db: db,
log: log,
}
err = mr.prepare(ctx)
if err != nil {
return fmt.Errorf("preparing meta repository: %w", err)
}
count, err := mr.run(ctx, units...)
if err != nil {
return fmt.Errorf("running transaction: %w", err)
}
if count > 0 {
xcontext.LogInfo(ctx, log, "some new migrations has been applied", slog.Int("count", count))
} else {
xcontext.LogDebug(ctx, log, "no new migrations has been applied")
}
return nil
}
type metaRepository struct {
db *sql.DB
log *slog.Logger
lastAppliedNumber int
}
func (r *metaRepository) run(ctx context.Context, units ...migrationUnit) (count int, err error) {
idx, found := slices.BinarySearchFunc(units, r.lastAppliedNumber, func(mu migrationUnit, i int) int {
return cmp.Compare(mu.num, i)
})
if !found && r.lastAppliedNumber > 0 {
return 0, fmt.Errorf("migration %d stored in meta was not found in provided migrations", r.lastAppliedNumber)
} else if r.lastAppliedNumber > 0 {
idx++
}
xcontext.LogDebug(
ctx, r.log,
"starting to apply migrations",
slog.Int("last_applied_migration", r.lastAppliedNumber),
slog.Int("next_migration_idx", idx),
)
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelDefault,
ReadOnly: false,
})
if err != nil {
return 0, fmt.Errorf("starting transaction: %w", err)
}
defer func() {
var errtx error
if err != nil {
xcontext.LogError(ctx, r.log, "rolling back migration changes due to error")
errtx = tx.Rollback()
} else {
xcontext.LogDebug(ctx, r.log, "commiting migration changes")
errtx = tx.Commit()
}
err = errors.Join(err, errtx)
}()
for i := idx; i < len(units); i++ {
unit := units[i]
err = unit.apply(ctx, tx)
if err != nil {
return 0, fmt.Errorf("unable to apply migration %q: %w", unit.name, err)
}
err = r.adjustMigrationApplied(ctx, tx, unit)
if err != nil {
return 0, fmt.Errorf("storing migration process info: %w", err)
}
xcontext.LogInfo(
ctx, r.log, "migration unit applied",
slog.Int("number", unit.num),
slog.String("name", unit.name),
)
count++
}
return count, nil
}
func (r *metaRepository) prepare(ctx context.Context) error {
err := r.makeTable(ctx)
if err != nil {
return fmt.Errorf("making table: %w", err)
}
err = r.loadLastAppliedMigration(ctx)
if err != nil {
return fmt.Errorf("loading last applied migration: %w", err)
}
return nil
}
func (r *metaRepository) makeTable(ctx context.Context) error {
const query = `CREATE TABLE IF NOT EXISTS migration_meta (` +
` id INT PRIMARY KEY NOT NULL` +
`, name TEXT NOT NULL` +
`, applied_at INT NOT NULL` +
`);`
_, err := r.db.ExecContext(ctx, query)
if err != nil {
return fmt.Errorf("executing query: %w", err)
}
return nil
}
func (r *metaRepository) loadLastAppliedMigration(ctx context.Context) error {
const query = `SELECT COALESCE(MAX(id), 0) FROM migration_meta;`
err := r.db.QueryRowContext(ctx, query).Scan(&r.lastAppliedNumber)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("executing query: %w", err)
}
}
return nil
}
func (r *metaRepository) adjustMigrationApplied(ctx context.Context, tx *sql.Tx, unit migrationUnit) error {
const query = `INSERT INTO migration_meta (id, name, applied_at) VALUES (?, ?, ?)`
args := []any{
unit.num,
unit.name,
time.Now().Truncate(time.Second).Unix(),
}
_, err := tx.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("executing query: %w", err)
}
r.lastAppliedNumber = unit.num
return nil
}

331
tags
View File

@ -1,331 +0,0 @@
!_TAG_EXTRA_DESCRIPTION anonymous /Include tags for non-named objects like lambda/
!_TAG_EXTRA_DESCRIPTION fileScope /Include tags of file scope/
!_TAG_EXTRA_DESCRIPTION pseudo /Include pseudo tags/
!_TAG_EXTRA_DESCRIPTION subparser /Include tags generated by subparsers/
!_TAG_FIELD_DESCRIPTION epoch /the last modified time of the input file (only for F\/file kind tag)/
!_TAG_FIELD_DESCRIPTION file /File-restricted scoping/
!_TAG_FIELD_DESCRIPTION input /input file/
!_TAG_FIELD_DESCRIPTION name /tag name/
!_TAG_FIELD_DESCRIPTION pattern /pattern/
!_TAG_FIELD_DESCRIPTION typeref /Type and name of a variable or typedef/
!_TAG_FIELD_DESCRIPTION!Go package /the real package specified by the package name/
!_TAG_FIELD_DESCRIPTION!Go packageName /the name for referring the package/
!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
!_TAG_KIND_DESCRIPTION!DTD E,entity /entities/
!_TAG_KIND_DESCRIPTION!DTD a,attribute /attributes/
!_TAG_KIND_DESCRIPTION!DTD e,element /elements/
!_TAG_KIND_DESCRIPTION!DTD n,notation /notations/
!_TAG_KIND_DESCRIPTION!DTD p,parameterEntity /parameter entities/
!_TAG_KIND_DESCRIPTION!Go M,anonMember /struct anonymous members/
!_TAG_KIND_DESCRIPTION!Go P,packageName /name for specifying imported package/
!_TAG_KIND_DESCRIPTION!Go Y,unknown /unknown/
!_TAG_KIND_DESCRIPTION!Go a,talias /type aliases/
!_TAG_KIND_DESCRIPTION!Go c,const /constants/
!_TAG_KIND_DESCRIPTION!Go f,func /functions/
!_TAG_KIND_DESCRIPTION!Go i,interface /interfaces/
!_TAG_KIND_DESCRIPTION!Go m,member /struct members/
!_TAG_KIND_DESCRIPTION!Go n,methodSpec /interface method specification/
!_TAG_KIND_DESCRIPTION!Go p,package /packages/
!_TAG_KIND_DESCRIPTION!Go s,struct /structs/
!_TAG_KIND_DESCRIPTION!Go t,type /types/
!_TAG_KIND_DESCRIPTION!Go v,var /variables/
!_TAG_OUTPUT_EXCMD mixed /number, pattern, mixed, or combineV2/
!_TAG_OUTPUT_FILESEP slash /slash or backslash/
!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/
!_TAG_OUTPUT_VERSION 0.0 /current.age/
!_TAG_PARSER_VERSION!DTD 0.0 /current.age/
!_TAG_PARSER_VERSION!Go 0.0 /current.age/
!_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/
!_TAG_PROC_CWD /home/pi/go/src/git.loyso.art/frx/kurious/ //
!_TAG_PROGRAM_AUTHOR Universal Ctags Team //
!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/
!_TAG_PROGRAM_URL https://ctags.io/ /official site/
!_TAG_PROGRAM_VERSION 6.0.0 /c480d71e/
!_TAG_ROLE_DESCRIPTION!DTD!element attOwner /attributes owner/
!_TAG_ROLE_DESCRIPTION!DTD!parameterEntity condition /conditions/
!_TAG_ROLE_DESCRIPTION!DTD!parameterEntity elementName /element names/
!_TAG_ROLE_DESCRIPTION!DTD!parameterEntity partOfAttDef /part of attribute definition/
!_TAG_ROLE_DESCRIPTION!Go!package imported /imported package/
!_TAG_ROLE_DESCRIPTION!Go!unknown receiverType /receiver type/
APIGatewayURL internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ APIGatewayURL string `json:"apiGatewayUrl"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
Address internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Address string `json:"address"`$/;" m struct:sravni.Contacts typeref:typename:string
Advertising internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Advertising CourseAdvertising `json:"advertising"`$/;" m struct:sravni.Course typeref:typename:CourseAdvertising
AdvertisingOnly internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ AdvertisingOnly bool `json:"advertisingOnly"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:bool
Alias internal/domain/kurious/kurious.go /^ Alias string$/;" m struct:kurious.Organization typeref:typename:string
Alias internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Alias string `json:"alias"`$/;" m struct:sravni.Organization typeref:typename:string
Alias internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Alias string `json:"alias"`$/;" m struct:sravni.ReduxDictionaryContainer typeref:typename:string
Approved internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Approved int `json:"approved"`$/;" m struct:sravni.RatingsInfo typeref:typename:int
BrandingURL internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ BrandingURL string `json:"brandingUrl"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
BuildID internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ BuildID string `json:"buildId"`$/;" m struct:sravni.PageState typeref:typename:string
BuildTime kurious.go /^func BuildTime() time.Time {$/;" f package:kurious typeref:typename:time.Time
ButtonMobileText internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ ButtonMobileText string `json:"buttonMobileText"`$/;" m struct:sravni.CourseAdvertising typeref:typename:string
ButtonText internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ ButtonText string `json:"buttonText"`$/;" m struct:sravni.CourseAdvertising typeref:typename:string
Categories internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Categories struct {$/;" m struct:sravni.InitialReduxState typeref:typename:struct { Data map[string]int `json:"data"`; }
Client internal/infrastructure/interfaceadapters/courses/sravni/client.go /^type Client interface {$/;" i package:sravni
Clone internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^func (p *PageState) Clone() *PageState {$/;" f struct:sravni.PageState typeref:typename:*PageState
Commit kurious.go /^func Commit() string {$/;" f package:kurious typeref:typename:string
ComplexCalculatedRatingValue internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ ComplexCalculatedRatingValue float64 `json:"complexCalculatedRatingValue"`$/;" m struct:sravni.RatingsInfo typeref:typename:float64
Contacts internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Contacts Contacts `json:"contacts"`$/;" m struct:sravni.Organization typeref:typename:Contacts
Contacts internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type Contacts struct {$/;" s package:sravni
Cost internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Cost float64 `json:"cost"`$/;" m struct:sravni.CourseAdvertising typeref:typename:float64
Course internal/domain/kurious/kurious.go /^type Course struct {$/;" s package:kurious
Course internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type Course struct {$/;" s package:sravni
CourseAdvertising internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type CourseAdvertising struct {$/;" s package:sravni
CourseDiscount internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type CourseDiscount struct {$/;" s package:sravni
CourseImage internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ CourseImage string `json:"courseImage"`$/;" m struct:sravni.Course typeref:typename:string
CoursesThematics internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ CoursesThematics []string `json:"coursesThematics"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:[]string
CoursesThematics internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ CoursesThematics string$/;" m struct:sravni.ListEducationProductsParams typeref:typename:string
Created internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Created time.Time `json:"created"`$/;" m struct:sravni.ReduxDictionaryContainer typeref:typename:time.Time
CreatedAt internal/domain/kurious/kurious.go /^ CreatedAt time.Time$/;" m struct:kurious.Course typeref:typename:time.Time
CreatedAt internal/domain/kurious/kurious.go /^ CreatedAt time.Time$/;" m struct:kurious.Organization typeref:typename:time.Time
Data internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Data struct {$/;" m struct:sravni.ReduxDictionaries typeref:typename:struct { CourseThematics ReduxDictionaryContainer `json:"coursesThematics"`; LearningType ReduxDictionaryContainer `json:"learningType"`; LearningTypeSelection ReduxDictionaryContainer `json:"learningTypeSelection"`; }
Data internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Data struct {$/;" m struct:sravni.ReduxMetadata typeref:typename:struct { Prefooter []ReduxStatePrefooterItem `json:"prefooter"`; }
DateStart internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ DateStart any `json:"dateStart"`$/;" m struct:sravni.Course typeref:typename:any
Debugf internal/infrastructure/interfaceadapters/courses/sravni/logger.go /^func (l restyCtxLogger) Debugf(format string, v ...any) {$/;" f struct:sravni.restyCtxLogger
DeletedAt internal/domain/kurious/kurious.go /^ DeletedAt nullable.Value[time.Time]$/;" m struct:kurious.Course typeref:typename:nullable.Value
DeletedAt internal/domain/kurious/kurious.go /^ DeletedAt nullable.Value[time.Time]$/;" m struct:kurious.Organization typeref:typename:nullable.Value
Description internal/domain/kurious/kurious.go /^ Description string$/;" m struct:kurious.Course typeref:typename:string
Dialog internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Dialog string `json:"dialog"`$/;" m struct:sravni.CourseAdvertising typeref:typename:string
Dictionaries internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Dictionaries ReduxDictionaries `json:"dictionaries"`$/;" m struct:sravni.InitialReduxState typeref:typename:ReduxDictionaries
DictionaryFormatFilterNew internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ DictionaryFormatFilterNew []string `json:"dictionaryFormatFilterNew"`$/;" m struct:sravni.Course typeref:typename:[]string
DictionaryLevelFilterNew internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ DictionaryLevelFilterNew []string `json:"dictionaryLevelFilterNew"`$/;" m struct:sravni.Course typeref:typename:[]string
Discount internal/domain/kurious/kurious.go /^ Discount float64$/;" m struct:kurious.Course typeref:typename:float64
Discount internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Discount CourseDiscount `json:"discount"`$/;" m struct:sravni.Course typeref:typename:CourseDiscount
Duration internal/domain/kurious/kurious.go /^ Duration time.Duration$/;" m struct:kurious.Course typeref:typename:time.Duration
EducationURL internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ EducationURL string `json:"educationUrl"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
EndDate internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ EndDate time.Time `json:"endDate"`$/;" m struct:sravni.CourseDiscount typeref:typename:time.Time
EndTime internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ EndTime any `json:"endTime"`$/;" m struct:sravni.CourseDiscount typeref:typename:any
Environment internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Environment string `json:"environment"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
ErrClientNotInited internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ ErrClientNotInited domain.SimpleError = "client was not inited"$/;" c package:sravni typeref:typename:domain.SimpleError
ErrNotImplemented internal/domain/error.go /^ ErrNotImplemented SimpleError = "not implemented"$/;" c package:domain typeref:typename:SimpleError
ErrUnexpectedStatus internal/domain/error.go /^ ErrUnexpectedStatus SimpleError = "unexpected status"$/;" c package:domain typeref:typename:SimpleError
Error internal/domain/error.go /^func (err *ValidationError) Error() string {$/;" f struct:domain.ValidationError typeref:typename:string
Error internal/domain/error.go /^func (err SimpleError) Error() string {$/;" f type:domain.SimpleError typeref:typename:string
Errorf internal/infrastructure/interfaceadapters/courses/sravni/logger.go /^func (l restyCtxLogger) Errorf(format string, v ...any) {$/;" f struct:sravni.restyCtxLogger
ExternalID internal/domain/kurious/kurious.go /^ ExternalID nullable.Value[string]$/;" m struct:kurious.Course typeref:typename:nullable.Value
ExternalID internal/domain/kurious/kurious.go /^ ExternalID nullable.Value[string]$/;" m struct:kurious.Organization typeref:typename:nullable.Value
Field internal/domain/error.go /^ Field string$/;" m struct:domain.ValidationError typeref:typename:string
Fields internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ Fields []string `json:"fields"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:[]string
Fields internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Fields []field `json:"fields"`$/;" m struct:sravni.ReduxDictionaryContainer typeref:typename:[]field
Fingerprint internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ Fingerprint string `json:"fingerPrint,omitempty"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:string
Full internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Full string `json:"full"`$/;" m struct:sravni.OrganizationName typeref:typename:string
FullPrice internal/domain/kurious/kurious.go /^ FullPrice float64$/;" m struct:kurious.Course typeref:typename:float64
Gateway internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Gateway string `json:"gatewayUrl"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
Genitive internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Genitive string `json:"genitive"`$/;" m struct:sravni.OrganizationName typeref:typename:string
GetMainPageState internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ GetMainPageState() *PageState$/;" n interface:sravni.Client typeref:typename:*PageState
GetMainPageState internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func (c *client) GetMainPageState() *PageState {$/;" f struct:sravni.client typeref:typename:*PageState
HasOffersID internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ HasOffersID string `json:"hasOffersId"`$/;" m struct:sravni.CourseAdvertising typeref:typename:string
ID internal/domain/kurious/kurious.go /^ ID string$/;" m struct:kurious.Organization typeref:typename:string
ID internal/domain/kurious/kurious.go /^ ID string$/;" m struct:kurious.Course typeref:typename:string
ID internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ ID string `json:"id"`$/;" m struct:sravni.Course typeref:typename:string
ID internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ ID string `json:"id"`$/;" m struct:sravni.Organization typeref:typename:string
ID internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ ID string `json:"_id"`$/;" m struct:sravni.ReduxDictionaryContainer typeref:typename:string
ImageLink internal/domain/kurious/kurious.go /^ ImageLink string$/;" m struct:kurious.Course typeref:typename:string
InitialReduxState internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ InitialReduxState InitialReduxState `json:"initialReduxState"`$/;" m struct:sravni.PageStateProperties typeref:typename:InitialReduxState
InitialReduxState internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type InitialReduxState struct {$/;" s package:sravni
IsLabsPartner internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ IsLabsPartner bool `json:"isLabsPartner"`$/;" m struct:sravni.Organization typeref:typename:bool
IsMix internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ IsMix bool `json:"isMix"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:bool
IsPartner internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ IsPartner bool `json:"isPartner"`$/;" m struct:sravni.CourseAdvertising typeref:typename:bool
IsTermApproximately internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ IsTermApproximately bool `json:"isTermApproximately"`$/;" m struct:sravni.Course typeref:typename:bool
Items internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ Items []Course `json:"items"`$/;" m struct:sravni.ListEducationProductsResponse typeref:typename:[]Course
Keywords internal/domain/kurious/kurious.go /^ Keywords []string$/;" m struct:kurious.Course typeref:typename:[]string
LabelText internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ LabelText string `json:"labelText"`$/;" m struct:sravni.CourseAdvertising typeref:typename:string
LearningType internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ LearningType []string `json:"learningtype"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:[]string
LearningType internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ LearningType string$/;" m struct:sravni.ListEducationProductsParams typeref:typename:string
Learningtype internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Learningtype []string `json:"learningtype"`$/;" m struct:sravni.Course typeref:typename:[]string
License internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ License string `json:"license"`$/;" m struct:sravni.Organization typeref:typename:string
Limit internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ Limit int `json:"limit"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:int
Limit internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ Limit int$/;" m struct:sravni.ListEducationProductsParams typeref:typename:int
Link internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Link string `json:"link"`$/;" m struct:sravni.Course typeref:typename:string
Link internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type Link struct {$/;" s package:sravni
Links internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Links []Link `json:"links"`$/;" m struct:sravni.ReduxStatePrefooterItem typeref:typename:[]Link
ListEducationProductsParams internal/infrastructure/interfaceadapters/courses/sravni/client.go /^type ListEducationProductsParams struct {$/;" s package:sravni
ListEducationProductsRequest internal/infrastructure/interfaceadapters/courses/sravni/client.go /^type ListEducationProductsRequest struct {$/;" s package:sravni
ListEducationProductsResponse internal/infrastructure/interfaceadapters/courses/sravni/client.go /^type ListEducationProductsResponse struct {$/;" s package:sravni
ListEducationalProducts internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ ListEducationalProducts($/;" n interface:sravni.Client typeref:typename:(result ListEducationProductsResponse, err error)
ListEducationalProducts internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func (c *client) ListEducationalProducts($/;" f struct:sravni.client typeref:typename:(result ListEducationProductsResponse, err error)
Location internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ Location string `json:"location"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:string
LogoLink internal/domain/kurious/kurious.go /^ LogoLink string$/;" m struct:kurious.Organization typeref:typename:string
Logotypes internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Logotypes struct {$/;" m struct:sravni.Organization typeref:typename:struct { Square string `json:"square"`; Web string `json:"web"`; Android string `json:"android"`; }
Map pkg/utilities/slices/map.go /^func Map[S any, E any](s []S, f func(S) E) []E {$/;" f package:slices typeref:typename:(s []S, f func(S) E) [
Metadata internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Metadata ReduxMetadata `json:"metadata"`$/;" m struct:sravni.InitialReduxState typeref:typename:ReduxMetadata
MixRepeated internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ MixRepeated bool `json:"mixRepeated"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:bool
Monetization internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Monetization struct {$/;" m struct:sravni.CourseAdvertising typeref:typename:struct { Pixels struct { Click string `json:"click"`; Display string `json:"display"`; } `json:"pixels"`; Kind string `json:"kind"`; }
Name internal/domain/kurious/kurious.go /^ Name string$/;" m struct:kurious.Organization typeref:typename:string
Name internal/domain/kurious/kurious.go /^ Name string$/;" m struct:kurious.Course typeref:typename:string
Name internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Name string `json:"name"`$/;" m struct:sravni.Course typeref:typename:string
Name internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Name OrganizationName `json:"name"`$/;" m struct:sravni.Organization typeref:typename:OrganizationName
Name internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Name string `json:"name"`$/;" m struct:sravni.ReduxDictionaryContainer typeref:typename:string
Name internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Name string `json:"name"`$/;" m struct:sravni.field typeref:typename:string
NewClient internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func NewClient(ctx context.Context, log *slog.Logger, debug bool) (c *client, err error) {$/;" f package:sravni typeref:typename:(c *client, err error)
NewServices internal/infrastructure/interfaceadapters/services.go /^func NewServices() Services {$/;" f package:adapters typeref:typename:Services
NewValidationError internal/domain/error.go /^func NewValidationError(field, reason string) *ValidationError {$/;" f package:domain typeref:typename:*ValidationError
NewValue internal/domain/nullable/value.go /^func NewValue[T any](value T) Value[T] {$/;" f package:nullable typeref:typename:(value T) Value
NewValuePtr internal/domain/nullable/value.go /^func NewValuePtr[T any](value *T) Value[T] {$/;" f package:nullable typeref:typename:(value *T) Value
NotB2B internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ NotB2B string `json:"not-b2b"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:string
NotSubIsWebinar internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ NotSubIsWebinar string `json:"not-sub-isWebinar"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:string
OfferHighlightColor internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ OfferHighlightColor string `json:"offerHighlightColor"`$/;" m struct:sravni.CourseAdvertising typeref:typename:string
OfferTypes internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ OfferTypes []string `json:"offerTypes"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:[]string
Offset internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ Offset int `json:"offset"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:int
Offset internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ Offset int$/;" m struct:sravni.ListEducationProductsParams typeref:typename:int
Organization internal/domain/kurious/kurious.go /^type Organization struct {$/;" s package:kurious
Organization internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Organization string `json:"organization"`$/;" m struct:sravni.Course typeref:typename:string
Organization internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type Organization struct {$/;" s package:sravni
OrganizationID internal/domain/kurious/kurious.go /^ OrganizationID string$/;" m struct:kurious.Course typeref:typename:string
OrganizationName internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type OrganizationName struct {$/;" s package:sravni
Organizations internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ Organizations map[string]Organization `json:"organizations"`$/;" m struct:sravni.ListEducationProductsResponse typeref:typename:map[string]Organization
OrgnazationURL internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ OrgnazationURL string `json:"organizationsUrl"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
OriginLink internal/domain/kurious/kurious.go /^ OriginLink string$/;" m struct:kurious.Course typeref:typename:string
Page internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Page string `json:"page"`$/;" m struct:sravni.PageState typeref:typename:string
PageState internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type PageState struct {$/;" s package:sravni
PageStateProperties internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type PageStateProperties struct {$/;" s package:sravni
PageStateRuntimeConfig internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type PageStateRuntimeConfig struct {$/;" s package:sravni
ParticipantsCount internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ ParticipantsCount int `json:"participantsCount"`$/;" m struct:sravni.RatingsInfo typeref:typename:int
Percent internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Percent int `json:"percent"`$/;" m struct:sravni.CourseDiscount typeref:typename:int
Phone internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Phone []string `json:"phone"`$/;" m struct:sravni.Contacts typeref:typename:[]string
PhoneVerifierURL internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ PhoneVerifierURL string `json:"phoneVerifierUrl"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
Prepositional internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Prepositional string `json:"prepositional"`$/;" m struct:sravni.OrganizationName typeref:typename:string
Price internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Price int `json:"price"`$/;" m struct:sravni.Course typeref:typename:int
PriceAll internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ PriceAll int `json:"priceAll"`$/;" m struct:sravni.Course typeref:typename:int
PriceInstallment internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ PriceInstallment int `json:"priceInstallment"`$/;" m struct:sravni.Course typeref:typename:int
ProductName internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ ProductName string `json:"productName,omitempty"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:string
PromoCode internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ PromoCode string `json:"promoCode"`$/;" m struct:sravni.CourseDiscount typeref:typename:string
PromoCodeType internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ PromoCodeType string `json:"promoCodeType"`$/;" m struct:sravni.CourseDiscount typeref:typename:string
Props internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Props PageStateProperties `json:"props"`$/;" m struct:sravni.PageState typeref:typename:PageStateProperties
Query internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Query map[string]string `json:"query"`$/;" m struct:sravni.PageState typeref:typename:map[string]string
RatingsInfo internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ RatingsInfo RatingsInfo `json:"ratingsInfo"`$/;" m struct:sravni.Organization typeref:typename:RatingsInfo
RatingsInfo internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type RatingsInfo struct {$/;" s package:sravni
Reason internal/domain/error.go /^ Reason string$/;" m struct:domain.ValidationError typeref:typename:string
ReduxDictionaries internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type ReduxDictionaries struct {$/;" s package:sravni
ReduxDictionaryContainer internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type ReduxDictionaryContainer struct {$/;" s package:sravni
ReduxMetadata internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type ReduxMetadata struct {$/;" s package:sravni
ReduxStatePrefooterItem internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type ReduxStatePrefooterItem struct {$/;" s package:sravni
Release internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Release string `json:"release"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
RuntimeConfig internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ RuntimeConfig PageStateRuntimeConfig `json:"runtimeConfig"`$/;" m struct:sravni.PageState typeref:typename:PageStateRuntimeConfig
ServiceName internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ ServiceName string `json:"serviceName"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
Services internal/infrastructure/interfaceadapters/services.go /^type Services struct{}$/;" s package:adapters
Set internal/domain/nullable/value.go /^func (n *Value[T]) Set(value T) {$/;" f unknown:nullable.T
Short internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Short string `json:"short"`$/;" m struct:sravni.OrganizationName typeref:typename:string
SideBarBannerText internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ SideBarBannerText string `json:"sideBarBannerText"`$/;" m struct:sravni.CourseAdvertising typeref:typename:string
SimpleError internal/domain/error.go /^type SimpleError string$/;" t package:domain typeref:typename:string
Site internal/domain/kurious/kurious.go /^ Site string$/;" m struct:kurious.Organization typeref:typename:string
SortDirection internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ SortDirection string `json:"sortDirection"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:string
SortProperty internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ SortProperty string `json:"sortProperty"`$/;" m struct:sravni.ListEducationProductsRequest typeref:typename:string
SourceName internal/domain/kurious/kurious.go /^ SourceName nullable.Value[string]$/;" m struct:kurious.Course typeref:typename:nullable.Value
SourceType internal/domain/kurious/kurious.go /^ SourceType SourceType$/;" m struct:kurious.Course typeref:typename:SourceType
SourceType internal/domain/kurious/kurious.go /^type SourceType uint8$/;" t package:kurious typeref:typename:uint8
SourceTypeManual internal/domain/kurious/kurious.go /^ SourceTypeManual$/;" c package:kurious
SourceTypeParsed internal/domain/kurious/kurious.go /^ SourceTypeParsed$/;" c package:kurious
SourceTypeUnset internal/domain/kurious/kurious.go /^ SourceTypeUnset SourceType = iota$/;" c package:kurious typeref:type:SourceType
StartsAt internal/domain/kurious/kurious.go /^ StartsAt time.Time$/;" m struct:kurious.Course typeref:typename:time.Time
TimeAllDay internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ TimeAllDay any `json:"timeAllDay"`$/;" m struct:sravni.Course typeref:typename:any
TimeAllHour internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ TimeAllHour any `json:"timeAllHour"`$/;" m struct:sravni.Course typeref:typename:any
TimeAllMonth internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ TimeAllMonth int `json:"timeAllMonth"`$/;" m struct:sravni.Course typeref:typename:int
TimeStart internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ TimeStart any `json:"timeStart"`$/;" m struct:sravni.Course typeref:typename:any
Title internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Title string `json:"title"`$/;" m struct:sravni.Link typeref:typename:string
Title internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Title string `json:"title"`$/;" m struct:sravni.ReduxStatePrefooterItem typeref:typename:string
Token internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Token []struct {$/;" m struct:sravni.CourseAdvertising typeref:typename:[]struct { ID string `json:"_id"`; Token []string `json:"token"`; Updated time.Time `json:"updated"`; V int `json:"__v"`; }
TotalCount internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ TotalCount int `json:"totalCount"`$/;" m struct:sravni.ListEducationProductsResponse typeref:typename:int
TotalCountAdv internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ TotalCountAdv int `json:"totalCountAdv"`$/;" m struct:sravni.ListEducationProductsResponse typeref:typename:int
TrackingType internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ TrackingType string `json:"trackingType"`$/;" m struct:sravni.CourseAdvertising typeref:typename:string
URL internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ URL string `json:"url"`$/;" m struct:sravni.Link typeref:typename:string
Updated internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Updated time.Time `json:"updated"`$/;" m struct:sravni.ReduxDictionaryContainer typeref:typename:time.Time
UpdatedAt internal/domain/kurious/kurious.go /^ UpdatedAt time.Time$/;" m struct:kurious.Course typeref:typename:time.Time
UpdatedAt internal/domain/kurious/kurious.go /^ UpdatedAt time.Time$/;" m struct:kurious.Organization typeref:typename:time.Time
UserID internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ UserID string `json:"userId"`$/;" m struct:sravni.ReduxDictionaryContainer typeref:typename:string
Valid internal/domain/nullable/value.go /^func (n Value[T]) Valid() bool {$/;" f unknown:nullable.T typeref:typename:bool
ValidationError internal/domain/error.go /^type ValidationError struct {$/;" s package:domain
Value internal/domain/nullable/value.go /^func (n Value[T]) Value() T {$/;" f unknown:nullable.T typeref:typename:T
Value internal/domain/nullable/value.go /^type Value[T any] struct {$/;" t package:nullable typeref:typename:[T any] struct { value T; valid bool;}
Value internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ Value string `json:"value"`$/;" m struct:sravni.field typeref:typename:string
Values internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func (qs querySet) Values() []string {$/;" f struct:sravni.querySet typeref:typename:[]string
ValutPtr internal/domain/nullable/value.go /^func (n Value[T]) ValutPtr() *T {$/;" f unknown:nullable.T typeref:typename:*T
Version kurious.go /^func Version() string {$/;" f package:kurious typeref:typename:string
Warnf internal/infrastructure/interfaceadapters/courses/sravni/logger.go /^func (l restyCtxLogger) Warnf(format string, v ...any) {$/;" f struct:sravni.restyCtxLogger
WebPath internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ WebPath string `json:"webPath"`$/;" m struct:sravni.PageStateRuntimeConfig typeref:typename:string
WithoutDiscountPrice internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^ WithoutDiscountPrice int `json:"withoutDiscountPrice"`$/;" m struct:sravni.Course typeref:typename:int
action cmd/dev/sravnicli/products.go /^type action interface {$/;" i package:main
adapters internal/infrastructure/interfaceadapters/services.go /^package adapters$/;" p
app cmd/dev/sravnicli/main.go /^func app(ctx context.Context, log *slog.Logger) (exitCode int, err error) {$/;" f package:main typeref:typename:(exitCode int, err error)
asCLIAction cmd/dev/sravnicli/products.go /^func asCLIAction(a action) cli.Action {$/;" f package:main typeref:typename:cli.Action
baseAction cmd/dev/sravnicli/products.go /^ *baseAction$/;" M struct:main.listProductsAction typeref:typename:*baseAction
baseAction cmd/dev/sravnicli/products.go /^type baseAction struct {$/;" s package:main
baseURL internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ baseURL = "https:\/\/www.sravni.ru\/kursy"$/;" c package:sravni
buildTime kurious.go /^ buildTime = ""$/;" v package:kurious
buildTimeParseOnce kurious.go /^var buildTimeParseOnce sync.Once$/;" v package:kurious typeref:typename:sync.Once
buildTimeParsed kurious.go /^ buildTimeParsed = time.Time{}$/;" v package:kurious
cachedMainPageInfo internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ cachedMainPageInfo *PageState$/;" m struct:sravni.client typeref:typename:*PageState
checkClientInited internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func (c *client) checkClientInited() error {$/;" f struct:sravni.client typeref:typename:error
client cmd/dev/sravnicli/products.go /^ client sravni.Client$/;" m struct:main.baseAction typeref:typename:sravni.Client
client internal/infrastructure/interfaceadapters/courses/sravni/client.go /^type client struct {$/;" s package:sravni
commit kurious.go /^ commit = "unknown"$/;" v package:kurious
context cmd/dev/sravnicli/products.go /^ context() context.Context$/;" n interface:main.action typeref:typename:context.Context
context cmd/dev/sravnicli/products.go /^func (ba baseAction) context() context.Context {$/;" f struct:main.baseAction typeref:typename:context.Context
courseThematic cmd/dev/sravnicli/products.go /^ courseThematic string$/;" m struct:main.listProductsActionParams typeref:typename:string
courseThematicOptName cmd/dev/sravnicli/products.go /^ courseThematicOptName = "course_thematic"$/;" c package:main
courses internal/app/courses/client.go /^package courses$/;" p
ctx cmd/dev/sravnicli/products.go /^ ctx context.Context$/;" m struct:main.baseAction typeref:typename:context.Context
ctx internal/infrastructure/interfaceadapters/courses/sravni/logger.go /^ ctx context.Context$/;" m struct:sravni.restyCtxLogger typeref:typename:context.Context
debugOptName cmd/dev/sravnicli/core.go /^ debugOptName = "verbose"$/;" c package:main
defaultOutput cmd/dev/sravnicli/main.go /^var defaultOutput = os.Stdout$/;" v package:main
defaultProductFields internal/infrastructure/interfaceadapters/courses/sravni/client.go /^var defaultProductFields = must(educationProductFields.exactSubset($/;" v package:sravni
domain internal/domain/error.go /^package domain$/;" p
educationProductFields internal/infrastructure/interfaceadapters/courses/sravni/client.go /^var educationProductFields = newQuerySet($/;" v package:sravni
exactSubset internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func (qs querySet) exactSubset(values ...string) ([]string, error) {$/;" f struct:sravni.querySet typeref:typename:([]string, error)
field internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^type field struct {$/;" s package:sravni
findNode internal/infrastructure/interfaceadapters/courses/sravni/helpers.go /^func findNode(parent *html.Node, eq func(*html.Node) (found, deeper bool)) *html.Node {$/;" f package:sravni typeref:typename:*html.Node
getMainPageState internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func (c *client) getMainPageState(ctx context.Context) (*PageState, error) {$/;" f struct:sravni.client typeref:typename:(*PageState, error)
handle cmd/dev/sravnicli/products.go /^ handle() error$/;" n interface:main.action typeref:typename:error
handle cmd/dev/sravnicli/products.go /^func (a *listProductsAction) handle() error {$/;" f struct:main.listProductsAction typeref:typename:error
handle cmd/dev/sravnicli/products.go /^func (ba *baseAction) handle() error {$/;" f struct:main.baseAction typeref:typename:error
hasValue internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func (qs querySet) hasValue(value string) bool {$/;" f struct:sravni.querySet typeref:typename:bool
http internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ http *resty.Client$/;" m struct:sravni.client typeref:typename:*resty.Client
jsonOptName cmd/dev/sravnicli/core.go /^ jsonOptName = "json"$/;" c package:main
kurious internal/domain/kurious/kurious.go /^package kurious$/;" p
kurious internal/domain/kurious/repository.go /^package kurious$/;" p
kurious kurious.go /^package kurious$/;" p
learningType cmd/dev/sravnicli/products.go /^ learningType string$/;" m struct:main.listProductsActionParams typeref:typename:string
learningTypeOptName cmd/dev/sravnicli/products.go /^ learningTypeOptName = "learning_type"$/;" c package:main
limit cmd/dev/sravnicli/products.go /^ limit int$/;" m struct:main.listProductsActionParams typeref:typename:int
limitOption cmd/dev/sravnicli/core.go /^var limitOption = cli.NewOption("limit", "Limits amount of items to return").WithType(cli.TypeIn/;" v package:main
listProductsAction cmd/dev/sravnicli/products.go /^type listProductsAction struct {$/;" s package:main
listProductsActionParams cmd/dev/sravnicli/products.go /^type listProductsActionParams struct {$/;" s package:main
log cmd/dev/sravnicli/products.go /^ log *slog.Logger$/;" m struct:main.baseAction typeref:typename:*slog.Logger
log internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ log *slog.Logger$/;" m struct:sravni.client typeref:typename:*slog.Logger
log internal/infrastructure/interfaceadapters/courses/sravni/logger.go /^ log *slog.Logger$/;" m struct:sravni.restyCtxLogger typeref:typename:*slog.Logger
main cmd/cli/main.go /^func main() {$/;" f package:main
main cmd/cli/main.go /^package main$/;" p
main cmd/dev/sravnicli/core.go /^package main$/;" p
main cmd/dev/sravnicli/main.go /^func main() {$/;" f package:main
main cmd/dev/sravnicli/main.go /^package main$/;" p
main cmd/dev/sravnicli/products.go /^package main$/;" p
makeEducationURL internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func (c *client) makeEducationURL(path string) string {$/;" f struct:sravni.client typeref:typename:string
makeLogger cmd/dev/sravnicli/core.go /^func makeLogger(options map[string]string) *slog.Logger {$/;" f package:main typeref:typename:*slog.Logger
makeSravniClient cmd/dev/sravnicli/core.go /^func makeSravniClient(ctx context.Context, log *slog.Logger, options map[string]string) (sravni./;" f package:main typeref:typename:(sravni.Client, error)
mappedValues internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ mappedValues map[string]struct{}$/;" m struct:sravni.querySet typeref:typename:map[string]struct{}
must internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func must[T any](t T, err error) T {$/;" f package:sravni typeref:typename:(t T, err error) T
newBaseAction cmd/dev/sravnicli/products.go /^func newBaseAction(ctx context.Context) *baseAction {$/;" f package:main typeref:typename:*baseAction
newListProductAction cmd/dev/sravnicli/products.go /^func newListProductAction(ctx context.Context) cli.Action {$/;" f package:main typeref:typename:cli.Action
newQuerySet internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func newQuerySet(values ...string) querySet {$/;" f package:sravni typeref:typename:querySet
nullable internal/domain/nullable/value.go /^package nullable$/;" p
offset cmd/dev/sravnicli/products.go /^ offset int$/;" m struct:main.listProductsActionParams typeref:typename:int
offsetOption cmd/dev/sravnicli/core.go /^var offsetOption = cli.NewOption("offset", "Offsets items to return").WithType(cli.TypeInt)$/;" v package:main
params cmd/dev/sravnicli/products.go /^ params listProductsActionParams$/;" m struct:main.listProductsAction typeref:typename:listProductsActionParams
parse cmd/dev/sravnicli/products.go /^ parse(args []string, options map[string]string) error$/;" n interface:main.action typeref:typename:error
parse cmd/dev/sravnicli/products.go /^func (a *listProductsAction) parse(args []string, options map[string]string) error {$/;" f struct:main.listProductsAction typeref:typename:error
parse cmd/dev/sravnicli/products.go /^func (ba *baseAction) parse(_ []string, options map[string]string) (err error) {$/;" f struct:main.baseAction typeref:typename:(err error)
parsePageState internal/infrastructure/interfaceadapters/courses/sravni/client.go /^func (c *client) parsePageState(ctx context.Context, body io.Reader) (*PageState, error) {$/;" f struct:sravni.client typeref:typename:(*PageState, error)
querySet internal/infrastructure/interfaceadapters/courses/sravni/client.go /^type querySet struct {$/;" s package:sravni
restyCtxLogger internal/infrastructure/interfaceadapters/courses/sravni/logger.go /^type restyCtxLogger struct {$/;" s package:sravni
setupAPICommand cmd/dev/sravnicli/products.go /^func setupAPICommand(ctx context.Context) cli.Command {$/;" f package:main typeref:typename:cli.Command
setupCLI cmd/dev/sravnicli/main.go /^func setupCLI(ctx context.Context) cli.App {$/;" f package:main typeref:typename:cli.App
slices pkg/utilities/slices/map.go /^package slices$/;" p
sravni internal/infrastructure/interfaceadapters/courses/sravni/client.go /^package sravni$/;" p
sravni internal/infrastructure/interfaceadapters/courses/sravni/entities.go /^package sravni$/;" p
sravni internal/infrastructure/interfaceadapters/courses/sravni/helpers.go /^package sravni$/;" p
sravni internal/infrastructure/interfaceadapters/courses/sravni/logger.go /^package sravni$/;" p
validCourseThematics internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ validCourseThematics querySet$/;" m struct:sravni.client typeref:typename:querySet
validLearningTypes internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ validLearningTypes querySet$/;" m struct:sravni.client typeref:typename:querySet
values internal/infrastructure/interfaceadapters/courses/sravni/client.go /^ values []string$/;" m struct:sravni.querySet typeref:typename:[]string
version kurious.go /^ version = "unknown"$/;" v package:kurious