Compare commits
10 Commits
f60ebcfb36
...
d5f76f4e07
| Author | SHA1 | Date | |
|---|---|---|---|
| d5f76f4e07 | |||
| d4974b30ec | |||
| 8b8a7618a2 | |||
| 067c63baa8 | |||
| 5fd0861e2d | |||
| 728c8fa59e | |||
| 2c0564f68c | |||
| 48f5d80f7a | |||
| fbe9927ac3 | |||
| 1d4e8e10fb |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
./assets/kurious binary
|
||||||
1
.task/checksum/generate
Normal file
1
.task/checksum/generate
Normal file
@ -0,0 +1 @@
|
|||||||
|
cf55887b91f81f789d59205c41f8368
|
||||||
20
Taskfile.yml
20
Taskfile.yml
@ -10,7 +10,7 @@ vars:
|
|||||||
GIT_VERSION:
|
GIT_VERSION:
|
||||||
sh: git tag | sort -r --version-sort | head -n1
|
sh: git tag | sort -r --version-sort | head -n1
|
||||||
BUILD_TIME:
|
BUILD_TIME:
|
||||||
sh: TZ=UTC date --iso-8601=seconds
|
sh: TZ=UTC date -u +"%Y-%m-%dT%H:%M:%SZ"
|
||||||
LDFLAGS:
|
LDFLAGS:
|
||||||
sh: echo '-X "{{.PROJECT}}.buildTime={{.BUILD_TIME}}" -X "{{.PROJECT}}.commit={{.GIT_COMMIT}}" -X "{{.PROJECT}}.version={{.GIT_VERSION}}"'
|
sh: echo '-X "{{.PROJECT}}.buildTime={{.BUILD_TIME}}" -X "{{.PROJECT}}.commit={{.GIT_COMMIT}}" -X "{{.PROJECT}}.version={{.GIT_VERSION}}"'
|
||||||
|
|
||||||
@ -18,15 +18,31 @@ tasks:
|
|||||||
install_tools:
|
install_tools:
|
||||||
cmds:
|
cmds:
|
||||||
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
|
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
|
||||||
|
- go install github.com/a-h/templ/cmd/templ@v0.2.513
|
||||||
|
generate:
|
||||||
|
cmds:
|
||||||
|
- "$GOBIN/templ generate"
|
||||||
|
sources:
|
||||||
|
- "internal/kurious/ports/http/templ/*.templ"
|
||||||
|
generates:
|
||||||
|
- "internal/kurious/ports/http/templ/*.go"
|
||||||
check:
|
check:
|
||||||
cmds:
|
cmds:
|
||||||
- "$GOBIN/golangci-lint run ./..."
|
- "$GOBIN/golangci-lint run ./..."
|
||||||
|
deps:
|
||||||
|
- generate
|
||||||
test:
|
test:
|
||||||
cmds:
|
cmds:
|
||||||
- go test ./internal/...
|
- go test ./internal/...
|
||||||
|
deps:
|
||||||
|
- generate
|
||||||
|
build_web:
|
||||||
|
cmds:
|
||||||
|
- go build -o $GOBIN/kuriousweb -v -ldflags '{{.LDFLAGS}}' cmd/kuriweb/*.go
|
||||||
|
deps: [check, test]
|
||||||
build_background:
|
build_background:
|
||||||
cmds:
|
cmds:
|
||||||
- go build -o $GOBIN/sravnibackground -v -ldflags '{{.LDFLAGS}}' cmd/background/*.go
|
- go build -o $GOBIN/kuriousbg -v -ldflags '{{.LDFLAGS}}' cmd/background/*.go
|
||||||
deps: [check, test]
|
deps: [check, test]
|
||||||
build_dev_cli:
|
build_dev_cli:
|
||||||
cmds:
|
cmds:
|
||||||
|
|||||||
6
assets/kurious/about.txt
Normal file
6
assets/kurious/about.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
This favicon was generated using the following font:
|
||||||
|
|
||||||
|
- Font Title: Lemon
|
||||||
|
- Font Author: Copyright 2011 The Lemon Project Authors (https://github.com/etunni/lemon) with Reserved Font Name "Lemon"
|
||||||
|
- Font Source: http://fonts.gstatic.com/s/lemon/v17/HI_EiYEVKqRMq0jBSZXAQ4-d.ttf
|
||||||
|
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))
|
||||||
BIN
assets/kurious/android-chrome-192x192.png
Normal file
BIN
assets/kurious/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
assets/kurious/android-chrome-512x512.png
Normal file
BIN
assets/kurious/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/kurious/apple-touch-icon.png
Normal file
BIN
assets/kurious/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
13
assets/kurious/embed.go
Normal file
13
assets/kurious/embed.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package kurious
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *
|
||||||
|
var root embed.FS
|
||||||
|
|
||||||
|
func AsHTTPFileHandler() http.Handler {
|
||||||
|
return http.FileServer(http.FS(root))
|
||||||
|
}
|
||||||
BIN
assets/kurious/favicon-16x16.png
Normal file
BIN
assets/kurious/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 343 B |
BIN
assets/kurious/favicon-32x32.png
Normal file
BIN
assets/kurious/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 797 B |
BIN
assets/kurious/favicon.ico
Normal file
BIN
assets/kurious/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
2
assets/kurious/robots.txt
Normal file
2
assets/kurious/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent:
|
||||||
|
Disallow: /
|
||||||
1
assets/kurious/site.webmanifest
Normal file
1
assets/kurious/site.webmanifest
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
6
assets/kurious/static/style.css
Normal file
6
assets/kurious/static/style.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.btn.btn-primary {
|
||||||
|
color: white;
|
||||||
|
background-color: black;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ import (
|
|||||||
"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/common/xcontext"
|
||||||
"git.loyso.art/frx/kurious/internal/common/xlog"
|
"git.loyso.art/frx/kurious/internal/common/xlog"
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/adapters"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/ports"
|
"git.loyso.art/frx/kurious/internal/kurious/ports"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||||
)
|
)
|
||||||
@ -43,19 +44,39 @@ func app(ctx context.Context) error {
|
|||||||
|
|
||||||
log := config.NewSLogger(cfg.Log)
|
log := config.NewSLogger(cfg.Log)
|
||||||
|
|
||||||
app, err := service.NewApplication(ctx, service.ApplicationConfig{
|
|
||||||
LogConfig: cfg.Log,
|
|
||||||
YDB: cfg.YDB,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("making new application: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sravniClient, err := sravni.NewClient(ctx, log, cfg.DebugHTTP)
|
sravniClient, err := sravni.NewClient(ctx, log, cfg.DebugHTTP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("making sravni client: %w", err)
|
return fmt.Errorf("making sravni client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainPageState, err := sravniClient.GetMainPageState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting main page state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionaries := mainPageState.Props.InitialReduxState.Dictionaries.Data
|
||||||
|
|
||||||
|
dictFieldsAsMap := func(fields ...sravni.Field) map[string]string {
|
||||||
|
out := make(map[string]string, len(fields))
|
||||||
|
for _, field := range fields {
|
||||||
|
out[field.Value] = field.Name
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
courseThematcisMapped := dictFieldsAsMap(dictionaries.CourseThematics.Fields...)
|
||||||
|
learningTypeMapped := dictFieldsAsMap(dictionaries.LearningType.Fields...)
|
||||||
|
|
||||||
|
mapper := adapters.NewMemoryMapper(courseThematcisMapped, learningTypeMapped)
|
||||||
|
|
||||||
|
app, err := service.NewApplication(ctx, service.ApplicationConfig{
|
||||||
|
LogConfig: cfg.Log,
|
||||||
|
YDB: cfg.YDB,
|
||||||
|
}, mapper)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("making new application: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
bgProcess := ports.NewBackgroundProcess(ctx, log)
|
bgProcess := ports.NewBackgroundProcess(ctx, log)
|
||||||
err = bgProcess.RegisterSyncSravniHandler(ctx, app, sravniClient, cfg.SyncSravniCron)
|
err = bgProcess.RegisterSyncSravniHandler(ctx, app, sravniClient, cfg.SyncSravniCron)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -72,13 +93,16 @@ func app(ctx context.Context) error {
|
|||||||
defer xcontext.LogInfo(ctx, log, "finished bprocess")
|
defer xcontext.LogInfo(ctx, log, "finished bprocess")
|
||||||
|
|
||||||
bgProcess.Run()
|
bgProcess.Run()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
xcontext.LogInfo(ctx, log, "running cancelation waiter")
|
xcontext.LogInfo(ctx, log, "running cancelation waiter")
|
||||||
defer xcontext.LogInfo(ctx, log, "finished cancelation waiter")
|
defer xcontext.LogInfo(ctx, log, "finished cancelation waiter")
|
||||||
|
|
||||||
<-egctx.Done()
|
<-egctx.Done()
|
||||||
|
|
||||||
sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*15)
|
sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*15)
|
||||||
defer sdcancel()
|
defer sdcancel()
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/client/sravni"
|
"git.loyso.art/frx/kurious/internal/common/client/sravni"
|
||||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||||
|
|
||||||
"github.com/teris-io/cli"
|
"github.com/teris-io/cli"
|
||||||
)
|
)
|
||||||
@ -188,7 +188,7 @@ func (a *productsFilterCountAction) parse(args []string, options map[string]stri
|
|||||||
filterNotEmpty := func(value string) bool {
|
filterNotEmpty := func(value string) bool {
|
||||||
return value != ""
|
return value != ""
|
||||||
}
|
}
|
||||||
a.params.courseThematic = xslice.Filter(
|
a.params.courseThematic = xslices.Filter(
|
||||||
strings.Split(options[courseThematicOptName], ","),
|
strings.Split(options[courseThematicOptName], ","),
|
||||||
filterNotEmpty,
|
filterNotEmpty,
|
||||||
)
|
)
|
||||||
|
|||||||
39
cmd/kuriweb/config.go
Normal file
39
cmd/kuriweb/config.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Log config.Log `json:"log"`
|
||||||
|
YDB config.YDB `json:"ydb"`
|
||||||
|
HTTP config.HTTP `json:"http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFromFile(path string, defaultConfigF func() Config) (Config, error) {
|
||||||
|
out := defaultConfigF()
|
||||||
|
payload, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("opening file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(payload, &out)
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("decoding as json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Log: config.Log{
|
||||||
|
Level: config.LogLevelInfo,
|
||||||
|
Format: config.LogFormatText,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
144
cmd/kuriweb/http.go
Normal file
144
cmd/kuriweb/http.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/assets/kurious"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/config"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/generator"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
|
xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makePathTemplate(params ...string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, param := range params {
|
||||||
|
sb.Grow(len(param) + 3)
|
||||||
|
|
||||||
|
sb.WriteRune('/')
|
||||||
|
sb.WriteRune('{')
|
||||||
|
sb.WriteString(param)
|
||||||
|
sb.WriteRune('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupHTTPWithTempl(srv xhttp.Server, router *mux.Router, log *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()
|
||||||
|
|
||||||
|
coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true)
|
||||||
|
coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet)
|
||||||
|
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
|
||||||
|
coursesRouter.HandleFunc(coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
|
||||||
|
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
|
||||||
|
coursesRouter.HandleFunc(coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
courseRouter := router.PathPrefix("/course").PathPrefix("/{course_id}").Subrouter()
|
||||||
|
courseRouter.HandleFunc("/", coursesAPI.Get).Methods(http.MethodGet)
|
||||||
|
courseRouter.HandleFunc("/short", coursesAPI.GetShort).Methods(http.MethodGet)
|
||||||
|
courseRouter.HandleFunc("/editdesc", coursesAPI.RenderEditDescription).Methods(http.MethodGet)
|
||||||
|
courseRouter.HandleFunc("/description", coursesAPI.UpdateCourseDescription).Methods(http.MethodPut)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server {
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
|
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Use(mux.CORSMethodMiddleware(router))
|
||||||
|
router.Use(middlewareLogger(log, cfg.Engine))
|
||||||
|
|
||||||
|
if cfg.Engine == "templ" {
|
||||||
|
setupHTTPWithTempl(srv, router, log)
|
||||||
|
} else {
|
||||||
|
setupHTTPWithGoTemplates(srv, router, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.MountLive {
|
||||||
|
fs := http.FileServer(http.Dir("./assets/kurious/static/"))
|
||||||
|
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
registerFile := func(filepath string) {
|
||||||
|
if !strings.HasPrefix(filepath, "/") {
|
||||||
|
filepath = "/" + filepath
|
||||||
|
}
|
||||||
|
|
||||||
|
relativePath := "./assets/kurious" + filepath
|
||||||
|
router.HandleFunc(filepath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, relativePath)
|
||||||
|
}).Methods(http.MethodGet)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range []string{
|
||||||
|
"robots.txt",
|
||||||
|
"android-chrome-192x192.png",
|
||||||
|
"android-chrome-512x512.png",
|
||||||
|
"apple-touch-icon.png",
|
||||||
|
"favicon-16x16.png",
|
||||||
|
"favicon-32x32.png",
|
||||||
|
"favicon.ico",
|
||||||
|
"site.webmanifest",
|
||||||
|
} {
|
||||||
|
registerFile(file)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs := kurious.AsHTTPFileHandler()
|
||||||
|
router.PathPrefix("/").Handler(fs).Methods(http.MethodGet)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Server{
|
||||||
|
Addr: cfg.ListenAddr,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareLogger(log *slog.Logger, engine string) mux.MiddlewareFunc {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
requestID := r.Header.Get("x-request-id")
|
||||||
|
if requestID == "" {
|
||||||
|
requestID = generator.RandomInt64ID()
|
||||||
|
}
|
||||||
|
ctx = xcontext.WithLogFields(
|
||||||
|
ctx,
|
||||||
|
slog.String("request_id", requestID),
|
||||||
|
slog.String("engine", engine),
|
||||||
|
)
|
||||||
|
|
||||||
|
xcontext.LogInfo(
|
||||||
|
ctx, log, "incoming request",
|
||||||
|
slog.String("method", r.Method),
|
||||||
|
slog.String("path", r.URL.Path),
|
||||||
|
)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
elapsed := time.Since(start).Truncate(time.Millisecond)
|
||||||
|
|
||||||
|
xcontext.LogInfo(
|
||||||
|
ctx, log, "request processed",
|
||||||
|
slog.Duration("elapsed", elapsed),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
120
cmd/kuriweb/main.go
Normal file
120
cmd/kuriweb/main.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/client/sravni"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/config"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/adapters"
|
||||||
|
xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http"
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := app(ctx)
|
||||||
|
if err != nil {
|
||||||
|
println(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func app(ctx context.Context) error {
|
||||||
|
var cfgpath string
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
cfgpath = os.Args[1]
|
||||||
|
} else {
|
||||||
|
cfgpath = "config.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := readFromFile(cfgpath, defaultConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading config from file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log := config.NewSLogger(cfg.Log)
|
||||||
|
|
||||||
|
sravniClient, err := sravni.NewClient(ctx, log, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to make new sravni client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainPageState, err := sravniClient.GetMainPageState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting main page state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionaries := mainPageState.Props.InitialReduxState.Dictionaries.Data
|
||||||
|
|
||||||
|
dictFieldsAsMap := func(fields ...sravni.Field) map[string]string {
|
||||||
|
out := make(map[string]string, len(fields))
|
||||||
|
for _, field := range fields {
|
||||||
|
out[field.Value] = field.Name
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
courseThematcisMapped := dictFieldsAsMap(dictionaries.CourseThematics.Fields...)
|
||||||
|
learningTypeMapped := dictFieldsAsMap(dictionaries.LearningType.Fields...)
|
||||||
|
|
||||||
|
mapper := adapters.NewMemoryMapper(courseThematcisMapped, learningTypeMapped)
|
||||||
|
|
||||||
|
app, err := service.NewApplication(ctx, service.ApplicationConfig{
|
||||||
|
LogConfig: cfg.Log,
|
||||||
|
YDB: cfg.YDB,
|
||||||
|
}, mapper)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("making new application: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpAPI := xhttp.NewServer(app, log.With(slog.String("component", "http")))
|
||||||
|
httpServer := setupHTTP(cfg.HTTP, httpAPI, log)
|
||||||
|
|
||||||
|
eg, egctx := errgroup.WithContext(ctx)
|
||||||
|
eg.Go(func() error {
|
||||||
|
xcontext.LogInfo(
|
||||||
|
ctx, log, "serving http",
|
||||||
|
slog.String("addr", httpServer.Addr),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := httpServer.ListenAndServe(); err != nil {
|
||||||
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return fmt.Errorf("listening http: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
eg.Go(func() error {
|
||||||
|
<-egctx.Done()
|
||||||
|
|
||||||
|
xcontext.LogInfo(ctx, log, "trying to shutdown http")
|
||||||
|
|
||||||
|
sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer sdcancel()
|
||||||
|
|
||||||
|
err := httpServer.Shutdown(sdctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shutting down the server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
xcontext.LogInfo(ctx, log, "server closed successfuly")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
||||||
19
go.mod
19
go.mod
@ -4,34 +4,31 @@ go 1.21
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-resty/resty/v2 v2.10.0
|
github.com/go-resty/resty/v2 v2.10.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/gorilla/mux v1.8.1
|
||||||
|
github.com/robfig/cron/v3 v3.0.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-yc v0.12.1
|
||||||
golang.org/x/net v0.18.0
|
golang.org/x/net v0.18.0
|
||||||
|
golang.org/x/sync v0.5.0
|
||||||
|
golang.org/x/time v0.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/a-h/templ v0.2.513 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.0.10 // 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/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/uuid v1.4.0 // indirect
|
github.com/google/uuid v1.4.0 // indirect
|
||||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.0 // indirect
|
|
||||||
github.com/stretchr/objx v0.5.0 // indirect
|
|
||||||
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 // indirect
|
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 // indirect
|
||||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd // indirect
|
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd // indirect
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 // indirect
|
|
||||||
github.com/ydb-platform/ydb-go-yc v0.12.1 // 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/sync v0.5.0 // indirect
|
|
||||||
golang.org/x/sys v0.14.0 // indirect
|
golang.org/x/sys v0.14.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
|
||||||
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect
|
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||||
google.golang.org/grpc v1.59.0 // indirect
|
google.golang.org/grpc v1.59.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
13
go.sum
13
go.sum
@ -515,6 +515,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
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.513/go.mod h1:9gZxTLtRzM3gQxO8jr09Na0v8/jfliS97S9W5SScanM=
|
||||||
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=
|
||||||
@ -572,8 +574,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
|
|||||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
|
||||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
|
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
|
||||||
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
|
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
|
||||||
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||||
@ -589,7 +589,6 @@ github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO
|
|||||||
github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
|
github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
|
||||||
github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||||
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 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ=
|
|
||||||
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=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||||
@ -609,6 +608,7 @@ 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=
|
||||||
@ -648,6 +648,7 @@ 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/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=
|
||||||
@ -691,6 +692,8 @@ github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMd
|
|||||||
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
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=
|
||||||
@ -756,7 +759,6 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
|
|||||||
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||||
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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
|
||||||
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/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=
|
||||||
@ -1092,7 +1094,6 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
|
|||||||
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 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
|
||||||
golang.org/x/time v0.3.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=
|
||||||
@ -1427,10 +1428,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
|||||||
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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
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=
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
"git.loyso.art/frx/kurious/pkg/slices"
|
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||||
|
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
@ -52,8 +52,8 @@ func NewClient(ctx context.Context, log *slog.Logger, debug bool) (c *client, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
getQuerySet := func(fields []field) querySet {
|
getQuerySet := func(fields []Field) querySet {
|
||||||
items := slices.Map(fields, func(f field) string {
|
items := xslices.Map(fields, func(f Field) string {
|
||||||
return f.Value
|
return f.Value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,7 @@ type ReduxMetadata struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type field struct {
|
type Field struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ type ReduxDictionaryContainer struct {
|
|||||||
UserID string `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
Fields []field `json:"fields"`
|
Fields []Field `json:"fields"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReduxDictionaries struct {
|
type ReduxDictionaries struct {
|
||||||
|
|||||||
@ -2,4 +2,6 @@ package config
|
|||||||
|
|
||||||
type HTTP struct {
|
type HTTP struct {
|
||||||
ListenAddr string `json:"listen_addr"`
|
ListenAddr string `json:"listen_addr"`
|
||||||
|
MountLive bool `json:"mount_live"`
|
||||||
|
Engine string `json:"engine"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ type YDB struct {
|
|||||||
DSN string
|
DSN string
|
||||||
Auth YCAuth
|
Auth YCAuth
|
||||||
ShutdownDuration time.Duration
|
ShutdownDuration time.Duration
|
||||||
|
DebugYDB bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ydb *YDB) UnmarshalJSON(data []byte) error {
|
func (ydb *YDB) UnmarshalJSON(data []byte) error {
|
||||||
|
|||||||
@ -19,19 +19,19 @@ func WithLogFields(ctx context.Context, fields ...slog.Attr) context.Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LogDebug(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
func LogDebug(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||||
log.LogAttrs(ctx, slog.LevelDebug, msg, append(attrs, getLogFields(ctx)...)...)
|
log.LogAttrs(ctx, slog.LevelDebug, msg, append(getLogFields(ctx), attrs...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogInfo(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
func LogInfo(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||||
log.LogAttrs(ctx, slog.LevelInfo, msg, append(attrs, getLogFields(ctx)...)...)
|
log.LogAttrs(ctx, slog.LevelInfo, msg, append(getLogFields(ctx), attrs...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogWarn(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
func LogWarn(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||||
log.LogAttrs(ctx, slog.LevelWarn, msg, append(attrs, getLogFields(ctx)...)...)
|
log.LogAttrs(ctx, slog.LevelWarn, msg, append(getLogFields(ctx), attrs...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogError(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
func LogError(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||||
log.LogAttrs(ctx, slog.LevelError, msg, append(attrs, getLogFields(ctx)...)...)
|
log.LogAttrs(ctx, slog.LevelError, msg, append(getLogFields(ctx), attrs...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogWithWarnError(ctx context.Context, log *slog.Logger, err error, msg string, attrs ...slog.Attr) {
|
func LogWithWarnError(ctx context.Context, log *slog.Logger, err error, msg string, attrs ...slog.Attr) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package xslice
|
package xslices
|
||||||
|
|
||||||
func Filter[T any](values []T, f func(T) bool) []T {
|
func Filter[T any](values []T, f func(T) bool) []T {
|
||||||
out := make([]T, 0, len(values))
|
out := make([]T, 0, len(values))
|
||||||
@ -1,9 +1,9 @@
|
|||||||
package xslice_test
|
package xslices_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFilterInplace(t *testing.T) {
|
func TestFilterInplace(t *testing.T) {
|
||||||
@ -43,7 +43,7 @@ func TestFilterInplace(t *testing.T) {
|
|||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
gotLen := xslice.FilterInplace(tc.in, tc.check)
|
gotLen := xslices.FilterInplace(tc.in, tc.check)
|
||||||
if gotLen != tc.expLen {
|
if gotLen != tc.expLen {
|
||||||
t.Errorf("exp %d got %d", tc.expLen, gotLen)
|
t.Errorf("exp %d got %d", tc.expLen, gotLen)
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package xslice
|
package xslices
|
||||||
|
|
||||||
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 {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package xslice
|
package xslices
|
||||||
|
|
||||||
func Map[T, U any](in []T, f func(T) U) []U {
|
func Map[T, U any](in []T, f func(T) U) []U {
|
||||||
out := make([]U, len(in))
|
out := make([]U, len(in))
|
||||||
21
internal/kurious/adapters/memory_mapper.go
Normal file
21
internal/kurious/adapters/memory_mapper.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package adapters
|
||||||
|
|
||||||
|
type inMemoryMapper struct {
|
||||||
|
courseThematicsByID map[string]string
|
||||||
|
learningTypeByID map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemoryMapper(courseThematics, learningType map[string]string) inMemoryMapper {
|
||||||
|
return inMemoryMapper{
|
||||||
|
courseThematicsByID: courseThematics,
|
||||||
|
learningTypeByID: learningType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m inMemoryMapper) CourseThematicNameByID(id string) string {
|
||||||
|
return m.courseThematicsByID[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m inMemoryMapper) LearningTypeNameByID(id string) string {
|
||||||
|
return m.learningTypeByID[id]
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
@ -17,13 +18,38 @@ import (
|
|||||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||||
|
|
||||||
"github.com/ydb-platform/ydb-go-sdk/v3"
|
"github.com/ydb-platform/ydb-go-sdk/v3"
|
||||||
|
ydblog "github.com/ydb-platform/ydb-go-sdk/v3/log"
|
||||||
"github.com/ydb-platform/ydb-go-sdk/v3/table"
|
"github.com/ydb-platform/ydb-go-sdk/v3/table"
|
||||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/options"
|
"github.com/ydb-platform/ydb-go-sdk/v3/table/options"
|
||||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/result/named"
|
"github.com/ydb-platform/ydb-go-sdk/v3/table/result/named"
|
||||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/types"
|
"github.com/ydb-platform/ydb-go-sdk/v3/table/types"
|
||||||
|
"github.com/ydb-platform/ydb-go-sdk/v3/trace"
|
||||||
yc "github.com/ydb-platform/ydb-go-yc"
|
yc "github.com/ydb-platform/ydb-go-yc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var coursesFields = []string{
|
||||||
|
"id",
|
||||||
|
"external_id",
|
||||||
|
"source_type",
|
||||||
|
"source_name",
|
||||||
|
"course_thematic",
|
||||||
|
"learning_type",
|
||||||
|
"organization_id",
|
||||||
|
"origin_link",
|
||||||
|
"image_link",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"full_price",
|
||||||
|
"discount",
|
||||||
|
"duration",
|
||||||
|
"starts_at",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"deleted_at",
|
||||||
|
}
|
||||||
|
|
||||||
|
var coursesFieldsStr = strings.Join(coursesFields, ",")
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultShutdownTimeout = time.Second * 10
|
defaultShutdownTimeout = time.Second * 10
|
||||||
)
|
)
|
||||||
@ -36,7 +62,7 @@ type YDBConnection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewYDBConnection(ctx context.Context, cfg config.YDB, log *slog.Logger) (*YDBConnection, error) {
|
func NewYDBConnection(ctx context.Context, cfg config.YDB, log *slog.Logger) (*YDBConnection, error) {
|
||||||
opts := make([]ydb.Option, 0, 2)
|
opts := make([]ydb.Option, 0, 3)
|
||||||
switch auth := cfg.Auth.(type) {
|
switch auth := cfg.Auth.(type) {
|
||||||
case config.YCAuthIAMToken:
|
case config.YCAuthIAMToken:
|
||||||
opts = append(opts, ydb.WithAccessTokenCredentials(auth.Token))
|
opts = append(opts, ydb.WithAccessTokenCredentials(auth.Token))
|
||||||
@ -46,6 +72,12 @@ func NewYDBConnection(ctx context.Context, cfg config.YDB, log *slog.Logger) (*Y
|
|||||||
yc.WithServiceAccountKeyFileCredentials(auth.Path),
|
yc.WithServiceAccountKeyFileCredentials(auth.Path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if cfg.DebugYDB {
|
||||||
|
opts = append(opts,
|
||||||
|
ydb.WithLogger(ydblog.Default(os.Stdout, ydblog.WithMinLevel(ydblog.DEBUG)), trace.DetailsAll),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
db, err := ydb.Open(
|
db, err := ydb.Open(
|
||||||
ctx,
|
ctx,
|
||||||
cfg.DSN,
|
cfg.DSN,
|
||||||
@ -54,6 +86,14 @@ func NewYDBConnection(ctx context.Context, cfg config.YDB, log *slog.Logger) (*Y
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("opening connection: %w", err)
|
return nil, fmt.Errorf("opening connection: %w", err)
|
||||||
}
|
}
|
||||||
|
endpoints, err := db.Discovery().Discover(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("discovering endpoints: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
xcontext.LogInfo(ctx, log, "discovered endpoint", slog.String("value", endpoint.Address()))
|
||||||
|
}
|
||||||
|
|
||||||
return &YDBConnection{
|
return &YDBConnection{
|
||||||
Driver: db,
|
Driver: db,
|
||||||
@ -87,59 +127,31 @@ func (r *ydbCourseRepository) List(
|
|||||||
) (result domain.ListCoursesResult, err error) {
|
) (result domain.ListCoursesResult, err error) {
|
||||||
const limit = 1000
|
const limit = 1000
|
||||||
const queryName = "list"
|
const queryName = "list"
|
||||||
// const query = `
|
|
||||||
// DECLARE $limit AS Int32;
|
|
||||||
// DECLARE $id AS Text;
|
|
||||||
// SELECT
|
|
||||||
// id,
|
|
||||||
// external_id,
|
|
||||||
// source_type,
|
|
||||||
// source_name,
|
|
||||||
// course_thematic,
|
|
||||||
// learning_type,
|
|
||||||
// organization_id,
|
|
||||||
// origin_link,
|
|
||||||
// image_link,
|
|
||||||
// name,
|
|
||||||
// description,
|
|
||||||
// full_price,
|
|
||||||
// discount,
|
|
||||||
// duration,
|
|
||||||
// starts_at,
|
|
||||||
// created_at,
|
|
||||||
// updated_at,
|
|
||||||
// deleted_at
|
|
||||||
// FROM
|
|
||||||
// courses
|
|
||||||
// WHERE
|
|
||||||
// id > $id
|
|
||||||
// ORDER BY id
|
|
||||||
// LIMIT $limit;`
|
|
||||||
//
|
|
||||||
const fields = `id, external_id, source_type, source_name, course_thematic, learning_type, organization_id, origin_link, image_link, name, description, full_price, discount, duration, starts_at, created_at, updated_at, deleted_at`
|
|
||||||
|
|
||||||
if params.Limit == 0 {
|
if params.Limit == 0 {
|
||||||
params.Limit = limit
|
params.Limit = limit
|
||||||
}
|
}
|
||||||
|
|
||||||
qtParams := queryTemplateParams{
|
qtParams := queryTemplateParams{
|
||||||
Fields: fields,
|
Fields: coursesFieldsStr,
|
||||||
Table: "courses",
|
Table: "courses",
|
||||||
Suffix: "ORDER BY id\nLIMIT $limit",
|
Suffix: "ORDER BY learning_type,course_thematic,id\nLIMIT $limit",
|
||||||
Declares: []queryTemplateDeclaration{{
|
Declares: []queryTemplateDeclaration{
|
||||||
Name: "limit",
|
{
|
||||||
Type: "Int32",
|
Name: "limit",
|
||||||
}, {
|
Type: "Int32",
|
||||||
Name: "id",
|
},
|
||||||
Type: "Text",
|
{
|
||||||
}},
|
Name: "id",
|
||||||
|
Type: "Text",
|
||||||
|
},
|
||||||
|
},
|
||||||
Conditions: []string{
|
Conditions: []string{
|
||||||
"id > $id",
|
"id > $id",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
options := make([]table.ParameterOption, 0, 4)
|
opts := make([]table.ParameterOption, 0, 4)
|
||||||
appendParams := func(name string, value string) {
|
appendTextParam := func(name string, value string) {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -151,18 +163,23 @@ func (r *ydbCourseRepository) List(
|
|||||||
}
|
}
|
||||||
qtParams.Declares = append(qtParams.Declares, d)
|
qtParams.Declares = append(qtParams.Declares, d)
|
||||||
qtParams.Conditions = append(qtParams.Conditions, d.Name+"="+d.Arg())
|
qtParams.Conditions = append(qtParams.Conditions, d.Name+"="+d.Arg())
|
||||||
options = append(options, table.ValueParam(d.Arg(), ydbvalue))
|
opts = append(opts, table.ValueParam(d.Arg(), ydbvalue))
|
||||||
}
|
}
|
||||||
appendParams("course_thematic", params.CourseThematic)
|
appendTextParam("course_thematic", params.CourseThematic)
|
||||||
appendParams("learning_type", params.LearningType)
|
appendTextParam("learning_type", params.LearningType)
|
||||||
|
|
||||||
var sb strings.Builder
|
opts = append(
|
||||||
err = template.Must(template.New("").Parse(queryTemplateSelect)).Execute(&sb, qtParams)
|
opts,
|
||||||
|
table.ValueParam("$id", types.TextValue(params.NextPageToken)),
|
||||||
|
table.ValueParam("$limit", types.Int32Value(int32(params.Limit))),
|
||||||
|
)
|
||||||
|
|
||||||
|
query, err := qtParams.render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("executing template: %w", err)
|
return result, fmt.Errorf("rendering query params: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
query := sb.String()
|
xcontext.LogInfo(ctx, r.log, "query prepared", slog.String("query", query), slog.String("args", tableParamOptsToString(opts...)))
|
||||||
|
|
||||||
courses := make([]domain.Course, 0, 1_000)
|
courses := make([]domain.Course, 0, 1_000)
|
||||||
readTx := table.TxControl(
|
readTx := table.TxControl(
|
||||||
@ -171,12 +188,13 @@ func (r *ydbCourseRepository) List(
|
|||||||
),
|
),
|
||||||
table.CommitTx(),
|
table.CommitTx(),
|
||||||
)
|
)
|
||||||
|
|
||||||
err = r.db.Table().Do(
|
err = r.db.Table().Do(
|
||||||
ctx,
|
ctx,
|
||||||
func(ctx context.Context, s table.Session) error {
|
func(ctx context.Context, s table.Session) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
since := time.Since(start)
|
since := time.Since(start).Truncate(time.Millisecond)
|
||||||
xcontext.LogInfo(
|
xcontext.LogInfo(
|
||||||
ctx, r.log,
|
ctx, r.log,
|
||||||
"executed query",
|
"executed query",
|
||||||
@ -185,53 +203,227 @@ func (r *ydbCourseRepository) List(
|
|||||||
)
|
)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var lastKnownID string
|
queryParams := table.NewQueryParameters(opts...)
|
||||||
for {
|
|
||||||
queryParams := table.NewQueryParameters(
|
|
||||||
table.ValueParam("$limit", types.Int32Value(limit)),
|
|
||||||
table.ValueParam("$id", types.TextValue(lastKnownID)),
|
|
||||||
)
|
|
||||||
_, res, err := s.Execute(
|
|
||||||
ctx, readTx, query, queryParams,
|
|
||||||
options.WithCollectStatsModeBasic(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("executing: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
_, res, err := s.Execute(
|
||||||
break
|
ctx, readTx, query, queryParams,
|
||||||
}
|
options.WithCollectStatsModeBasic(),
|
||||||
|
)
|
||||||
for res.NextRow() {
|
if err != nil {
|
||||||
var cdb courseDB
|
return fmt.Errorf("executing: %w", err)
|
||||||
err = res.ScanNamed(cdb.getNamedValues()...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("scanning row: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
courses = append(courses, mapCourseDB(cdb))
|
|
||||||
}
|
|
||||||
if err = res.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastKnownID = courses[len(courses)-1].ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for res.NextRow() {
|
||||||
|
var cdb courseDB
|
||||||
|
err = res.ScanNamed(cdb.getNamedValues()...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("scanning row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
courses = append(courses, mapCourseDB(cdb))
|
||||||
|
}
|
||||||
|
if err = res.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.NextPageToken = courses[len(courses)-1].ID
|
||||||
|
xcontext.LogDebug(ctx, r.log, "scanned rows", slog.Int("count", len(courses)))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
table.WithIdempotent())
|
table.WithIdempotent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return domain.ListCoursesResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return courses, err
|
result.Courses = courses
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ydbCourseRepository) Get(ctx context.Context, id string) (course domain.Course, err error) {
|
func (r *ydbCourseRepository) ListLearningTypes(
|
||||||
const queryName = "get"
|
ctx context.Context,
|
||||||
|
) (result domain.ListLearningTypeResult, err error) {
|
||||||
|
const queryName = "list_learning_type"
|
||||||
|
const querySelect = `SELECT DISTINCT learning_type FROM courses;`
|
||||||
|
|
||||||
|
readTx := table.TxControl(
|
||||||
|
table.BeginTx(
|
||||||
|
table.WithOnlineReadOnly(),
|
||||||
|
),
|
||||||
|
table.CommitTx(),
|
||||||
|
)
|
||||||
|
|
||||||
|
err = r.db.Table().Do(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, s table.Session) error {
|
||||||
|
start := time.Now()
|
||||||
|
defer func() {
|
||||||
|
since := time.Since(start).Truncate(time.Millisecond)
|
||||||
|
xcontext.LogInfo(
|
||||||
|
ctx, r.log,
|
||||||
|
"executed query",
|
||||||
|
slog.String("name", queryName),
|
||||||
|
slog.Duration("elapsed", since),
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, res, err := s.Execute(
|
||||||
|
ctx, readTx, querySelect, table.NewQueryParameters(),
|
||||||
|
options.WithCollectStatsModeNone(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("executing query: %w", err)
|
||||||
|
}
|
||||||
|
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for res.NextRow() {
|
||||||
|
var learningTypeID string
|
||||||
|
if err = res.Scan(&learningTypeID); err != nil {
|
||||||
|
return fmt.Errorf("scanning row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.LearningTypeIDs = append(result.LearningTypeIDs, learningTypeID)
|
||||||
|
}
|
||||||
|
if err = res.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
xcontext.LogDebug(ctx, r.log, "scanned rows", slog.Int("count", len(result.LearningTypeIDs)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
table.WithIdempotent(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ydbCourseRepository) ListCourseThematics(
|
||||||
|
ctx context.Context,
|
||||||
|
params domain.ListCourseThematicsParams,
|
||||||
|
) (result domain.ListCourseThematicsResult, err error) {
|
||||||
|
const queryName = "list_course_thematics"
|
||||||
|
|
||||||
|
qtParams := queryTemplateParams{
|
||||||
|
Fields: "DISTINCT course_thematic",
|
||||||
|
Table: "courses",
|
||||||
|
Declares: []queryTemplateDeclaration{},
|
||||||
|
Conditions: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
learningTypeValue := types.TextValue(params.LearningTypeID)
|
||||||
|
d := queryTemplateDeclaration{
|
||||||
|
Name: "learning_type",
|
||||||
|
Type: learningTypeValue.Type().String(),
|
||||||
|
}
|
||||||
|
qtParams.Declares = append(qtParams.Declares, d)
|
||||||
|
qtParams.Conditions = append(qtParams.Conditions, d.Name+"="+d.Arg())
|
||||||
|
|
||||||
|
opts := []table.ParameterOption{
|
||||||
|
table.ValueParam(d.Arg(), learningTypeValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
query, err := qtParams.render()
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("rendering query params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
readTx := table.TxControl(
|
||||||
|
table.BeginTx(
|
||||||
|
table.WithOnlineReadOnly(),
|
||||||
|
),
|
||||||
|
table.CommitTx(),
|
||||||
|
)
|
||||||
|
|
||||||
|
err = r.db.Table().Do(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, s table.Session) error {
|
||||||
|
start := time.Now()
|
||||||
|
defer func() {
|
||||||
|
since := time.Since(start).Truncate(time.Millisecond)
|
||||||
|
xcontext.LogInfo(
|
||||||
|
ctx, r.log,
|
||||||
|
"executed query",
|
||||||
|
slog.String("name", queryName),
|
||||||
|
slog.Duration("elapsed", since),
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, res, err := s.Execute(
|
||||||
|
ctx, readTx, query, table.NewQueryParameters(opts...),
|
||||||
|
options.WithCollectStatsModeNone(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("executing query: %w", err)
|
||||||
|
}
|
||||||
|
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for res.NextRow() {
|
||||||
|
var courseThematicID string
|
||||||
|
if err = res.Scan(&courseThematicID); err != nil {
|
||||||
|
return fmt.Errorf("scanning row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.CourseThematicIDs = append(result.CourseThematicIDs, courseThematicID)
|
||||||
|
}
|
||||||
|
if err = res.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
xcontext.LogDebug(ctx, r.log, "scanned rows", slog.Int("count", len(result.CourseThematicIDs)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
table.WithIdempotent(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ydbCourseRepository) Get(
|
||||||
|
ctx context.Context,
|
||||||
|
id string,
|
||||||
|
) (course domain.Course, err error) {
|
||||||
|
const queryName = "get"
|
||||||
|
const querySelect = `DECLARE $id AS Text;
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
external_id,
|
||||||
|
source_type,
|
||||||
|
source_name,
|
||||||
|
course_thematic,
|
||||||
|
learning_type,
|
||||||
|
organization_id,
|
||||||
|
origin_link,
|
||||||
|
image_link,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
full_price,
|
||||||
|
discount,
|
||||||
|
duration,
|
||||||
|
starts_at,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
deleted_at
|
||||||
|
FROM
|
||||||
|
courses
|
||||||
|
WHERE
|
||||||
|
id = $id;`
|
||||||
|
|
||||||
courses := make([]domain.Course, 0, 1)
|
|
||||||
readTx := table.TxControl(
|
readTx := table.TxControl(
|
||||||
table.BeginTx(
|
table.BeginTx(
|
||||||
table.WithOnlineReadOnly(),
|
table.WithOnlineReadOnly(),
|
||||||
@ -255,73 +447,79 @@ func (r *ydbCourseRepository) Get(ctx context.Context, id string) (course domain
|
|||||||
_, res, err := s.Execute(
|
_, res, err := s.Execute(
|
||||||
ctx,
|
ctx,
|
||||||
readTx,
|
readTx,
|
||||||
`
|
querySelect,
|
||||||
DECLARE $id AS Text;
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
external_id,
|
|
||||||
source_type,
|
|
||||||
source_name,
|
|
||||||
course_thematic,
|
|
||||||
learning_type,
|
|
||||||
organization_id,
|
|
||||||
origin_link,
|
|
||||||
image_link,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
full_price,
|
|
||||||
discount,
|
|
||||||
duration,
|
|
||||||
starts_at,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
deleted_at
|
|
||||||
FROM
|
|
||||||
courses
|
|
||||||
WHERE
|
|
||||||
id = $id;
|
|
||||||
`,
|
|
||||||
table.NewQueryParameters(
|
table.NewQueryParameters(
|
||||||
table.ValueParam("$id", types.TextValue(id)),
|
table.ValueParam("$id", types.TextValue(id)),
|
||||||
),
|
),
|
||||||
options.WithCollectStatsModeBasic(),
|
options.WithCollectStatsModeBasic(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("executing: %w", err)
|
return fmt.Errorf("executing query: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for res.NextResultSet(ctx) {
|
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
||||||
for res.NextRow() {
|
return errors.ErrNotFound
|
||||||
var cdb courseDB
|
}
|
||||||
_ = res.ScanNamed(cdb.getNamedValues()...)
|
|
||||||
courses = append(courses, mapCourseDB(cdb))
|
for res.NextRow() {
|
||||||
|
var cdb courseDB
|
||||||
|
err = res.ScanNamed(cdb.getNamedValues()...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("scanning row: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
course = mapCourseDB(cdb)
|
||||||
}
|
}
|
||||||
if err = res.Err(); err != nil {
|
if err = res.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats := res.Stats()
|
||||||
|
xcontext.LogInfo(
|
||||||
|
ctx, r.log, "query stats",
|
||||||
|
slog.String("ast", stats.QueryAST()),
|
||||||
|
slog.String("plan", stats.QueryPlan()),
|
||||||
|
slog.Duration("total_cpu_time", stats.TotalCPUTime()),
|
||||||
|
slog.Duration("total_duration", stats.TotalDuration()),
|
||||||
|
slog.Duration("process_cpu_time", stats.ProcessCPUTime()),
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
table.WithIdempotent())
|
table.WithIdempotent(),
|
||||||
if err != nil {
|
)
|
||||||
return domain.Course{}, err
|
return course, err
|
||||||
}
|
|
||||||
|
|
||||||
if len(courses) == 0 {
|
|
||||||
return course, errors.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return courses[0], err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ydbCourseRepository) GetByExternalID(ctx context.Context, id string) (domain.Course, error) {
|
func (r *ydbCourseRepository) GetByExternalID(ctx context.Context, id string) (domain.Course, error) {
|
||||||
return domain.Course{}, nil
|
return domain.Course{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createCourseParamsAsStruct(params domain.CreateCourseParams) types.Value {
|
type updateCourseParams struct {
|
||||||
st := mapSourceTypeFromDomain(params.SourceType)
|
domain.CreateCourseParams
|
||||||
|
|
||||||
|
CreatedAt time.Time
|
||||||
|
DeletedAt nullable.Value[time.Time]
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCourseParamsAsStruct(params updateCourseParams) types.Value {
|
||||||
|
opts := createCourseParamsAsStructValues(params.CreateCourseParams)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return types.StructValue(
|
return types.StructValue(
|
||||||
|
append(
|
||||||
|
opts[:len(opts)-3],
|
||||||
|
types.StructFieldValue("created_at", types.DatetimeValueFromTime(params.CreatedAt)),
|
||||||
|
types.StructFieldValue("updated_at", types.DatetimeValueFromTime(now)),
|
||||||
|
types.StructFieldValue("deleted_at", types.NullableDatetimeValue(nil)),
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCourseParamsAsStructValues(params domain.CreateCourseParams) []types.StructValueOption {
|
||||||
|
st := mapSourceTypeFromDomain(params.SourceType)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
return []types.StructValueOption{
|
||||||
types.StructFieldValue("id", types.TextValue(params.ID)),
|
types.StructFieldValue("id", types.TextValue(params.ID)),
|
||||||
types.StructFieldValue("name", types.TextValue(params.Name)),
|
types.StructFieldValue("name", types.TextValue(params.Name)),
|
||||||
types.StructFieldValue("source_type", types.TextValue(st)),
|
types.StructFieldValue("source_type", types.TextValue(st)),
|
||||||
@ -340,11 +538,16 @@ func createCourseParamsAsStruct(params domain.CreateCourseParams) types.Value {
|
|||||||
types.StructFieldValue("created_at", types.DatetimeValueFromTime(now)),
|
types.StructFieldValue("created_at", types.DatetimeValueFromTime(now)),
|
||||||
types.StructFieldValue("updated_at", types.DatetimeValueFromTime(now)),
|
types.StructFieldValue("updated_at", types.DatetimeValueFromTime(now)),
|
||||||
types.StructFieldValue("deleted_at", types.NullableDatetimeValue(nil)),
|
types.StructFieldValue("deleted_at", types.NullableDatetimeValue(nil)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCourseParamsAsStruct(params domain.CreateCourseParams) types.Value {
|
||||||
|
return types.StructValue(
|
||||||
|
createCourseParamsAsStructValues(params)...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ydbCourseRepository) CreateBatch(ctx context.Context, params ...domain.CreateCourseParams) error {
|
func (r *ydbCourseRepository) CreateBatch(ctx context.Context, params ...domain.CreateCourseParams) error {
|
||||||
// -- PRAGMA TablePathPrefix("courses");
|
|
||||||
const upsertQuery = `DECLARE $courseData AS List<Struct<
|
const upsertQuery = `DECLARE $courseData AS List<Struct<
|
||||||
id: Text,
|
id: Text,
|
||||||
external_id: Optional<Text>,
|
external_id: Optional<Text>,
|
||||||
@ -423,6 +626,100 @@ func (r *ydbCourseRepository) Delete(ctx context.Context, id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ydbCourseRepository) UpdateCourseDescription(ctx context.Context, id, description string) error {
|
||||||
|
course, err := r.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting course: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := updateCourseParams{
|
||||||
|
CreateCourseParams: domain.CreateCourseParams{
|
||||||
|
ID: course.ID,
|
||||||
|
ExternalID: course.ExternalID,
|
||||||
|
Name: course.Name,
|
||||||
|
SourceType: course.SourceType,
|
||||||
|
SourceName: course.SourceName,
|
||||||
|
CourseThematic: course.Thematic,
|
||||||
|
LearningType: course.LearningType,
|
||||||
|
OrganizationID: course.OrganizationID,
|
||||||
|
OriginLink: course.OriginLink,
|
||||||
|
ImageLink: course.ImageLink,
|
||||||
|
Description: description,
|
||||||
|
FullPrice: course.FullPrice,
|
||||||
|
Discount: course.Discount,
|
||||||
|
Duration: course.Duration,
|
||||||
|
StartsAt: course.StartsAt,
|
||||||
|
},
|
||||||
|
CreatedAt: course.CreatedAt,
|
||||||
|
DeletedAt: course.DeletedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStruct := updateCourseParamsAsStruct(params)
|
||||||
|
|
||||||
|
const upsertQuery = `DECLARE $courseData AS List<Struct<
|
||||||
|
id: Text,
|
||||||
|
external_id: Optional<Text>,
|
||||||
|
name: Text,
|
||||||
|
source_type: Text,
|
||||||
|
source_name: Optional<Text>,
|
||||||
|
course_thematic: Text,
|
||||||
|
learning_type: Text,
|
||||||
|
organization_id: Text,
|
||||||
|
origin_link: Text,
|
||||||
|
image_link: Text,
|
||||||
|
description: Text,
|
||||||
|
full_price: Double,
|
||||||
|
discount: Double,
|
||||||
|
duration: Interval,
|
||||||
|
starts_at: Datetime,
|
||||||
|
created_at: Datetime,
|
||||||
|
updated_at: Datetime,
|
||||||
|
deleted_at: Optional<Datetime>>>;
|
||||||
|
|
||||||
|
REPLACE INTO
|
||||||
|
courses
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
external_id,
|
||||||
|
name,
|
||||||
|
source_type,
|
||||||
|
source_name,
|
||||||
|
course_thematic,
|
||||||
|
learning_type,
|
||||||
|
organization_id,
|
||||||
|
origin_link,
|
||||||
|
image_link,
|
||||||
|
description,
|
||||||
|
full_price,
|
||||||
|
discount,
|
||||||
|
duration,
|
||||||
|
starts_at,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
deleted_at
|
||||||
|
FROM AS_TABLE($courseData);`
|
||||||
|
|
||||||
|
writeTx := table.TxControl(
|
||||||
|
table.BeginTx(
|
||||||
|
table.WithSerializableReadWrite(),
|
||||||
|
),
|
||||||
|
table.CommitTx(),
|
||||||
|
)
|
||||||
|
err = r.db.Table().Do(ctx, func(ctx context.Context, s table.Session) error {
|
||||||
|
queryParams := table.NewQueryParameters(
|
||||||
|
table.ValueParam("$courseData", types.ListValue(updateStruct)),
|
||||||
|
)
|
||||||
|
_, _, err := s.Execute(ctx, writeTx, upsertQuery, queryParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("executing query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ydbCourseRepository) CreateCourseTable(ctx context.Context) error {
|
func (r *ydbCourseRepository) CreateCourseTable(ctx context.Context) error {
|
||||||
return r.db.Table().Do(ctx, func(ctx context.Context, s table.Session) error {
|
return r.db.Table().Do(ctx, func(ctx context.Context, s table.Session) error {
|
||||||
return s.CreateTable(
|
return s.CreateTable(
|
||||||
@ -537,8 +834,8 @@ func mapCourseDB(cdb courseDB) domain.Course {
|
|||||||
Name: cdb.Name,
|
Name: cdb.Name,
|
||||||
SourceType: st,
|
SourceType: st,
|
||||||
SourceName: nullable.NewValuePtr(cdb.SourceName),
|
SourceName: nullable.NewValuePtr(cdb.SourceName),
|
||||||
Thematic: cdb.CourseThematic,
|
ThematicID: cdb.CourseThematic,
|
||||||
LearningType: cdb.LearningType,
|
LearningTypeID: cdb.LearningType,
|
||||||
OrganizationID: cdb.OrganizationID,
|
OrganizationID: cdb.OrganizationID,
|
||||||
OriginLink: cdb.OriginLink,
|
OriginLink: cdb.OriginLink,
|
||||||
ImageLink: cdb.ImageLink,
|
ImageLink: cdb.ImageLink,
|
||||||
@ -579,9 +876,27 @@ type queryTemplateParams struct {
|
|||||||
Suffix string
|
Suffix string
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryTemplateSelect = `
|
func (p queryTemplateParams) render() (string, error) {
|
||||||
{{ range .Declares }}DECLARE ${{.Name}} AS {{.Type}}\n{{end}}
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(queryTemplateSelect) * 3)
|
||||||
|
|
||||||
|
err := querySelect.Execute(&sb, p)
|
||||||
|
return sb.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryTemplateSelect = `{{ range .Declares }}DECLARE ${{.Name}} AS {{.Type}};{{end}}
|
||||||
SELECT {{.Fields}}
|
SELECT {{.Fields}}
|
||||||
FROM {{.Table}}
|
FROM {{.Table}}
|
||||||
WHERE {{ range .Conditions }}{{.}}\n{{end}}
|
WHERE 1=1 {{ range .Conditions }} AND {{.}} {{ end }}
|
||||||
{{.Suffix}}`
|
{{.Suffix}}`
|
||||||
|
|
||||||
|
var querySelect = template.Must(template.New("").Parse(queryTemplateSelect))
|
||||||
|
|
||||||
|
func tableParamOptsToString(in ...table.ParameterOption) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, opt := range in {
|
||||||
|
sb.WriteString(opt.Name() + ":" + opt.Value().Yql() + ";")
|
||||||
|
// sb.WriteString(opt.Name() + " (" + opt.Value().Type().String() + "); ")
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|||||||
@ -6,14 +6,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Commands struct {
|
type Commands struct {
|
||||||
InsertCourses command.CreateCoursesHandler
|
InsertCourses command.CreateCoursesHandler
|
||||||
InsertCourse command.CreateCourseHandler
|
InsertCourse command.CreateCourseHandler
|
||||||
DeleteCourse command.DeleteCourseHandler
|
DeleteCourse command.DeleteCourseHandler
|
||||||
|
UpdateCourseDescription command.UpdateCourseDescriptionHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
type Queries struct {
|
type Queries struct {
|
||||||
GetCourse query.GetCourseHandler
|
GetCourse query.GetCourseHandler
|
||||||
ListCourses query.ListCourseHandler
|
ListCourses query.ListCourseHandler
|
||||||
|
ListLearningTypes query.ListLearningTypesHandler
|
||||||
|
ListCourseThematics query.ListCourseThematicsHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ func NewCreateCoursesHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h createCoursesHandler) Handle(ctx context.Context, cmd CreateCourses) error {
|
func (h createCoursesHandler) Handle(ctx context.Context, cmd CreateCourses) error {
|
||||||
params := xslice.Map(cmd.Courses, func(in CreateCourse) (out domain.CreateCourseParams) {
|
params := xslices.Map(cmd.Courses, func(in CreateCourse) (out domain.CreateCourseParams) {
|
||||||
return domain.CreateCourseParams(in)
|
return domain.CreateCourseParams(in)
|
||||||
})
|
})
|
||||||
err := h.repo.CreateBatch(ctx, params...)
|
err := h.repo.CreateBatch(ctx, params...)
|
||||||
|
|||||||
35
internal/kurious/app/command/updatecoursedescription.go
Normal file
35
internal/kurious/app/command/updatecoursedescription.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateCourseDescription struct {
|
||||||
|
ID string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateCourseDescriptionHandler decorator.CommandHandler[UpdateCourseDescription]
|
||||||
|
|
||||||
|
type updateCourseDescriptionHandler struct {
|
||||||
|
repo domain.CourseRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateCourseDescriptionHandler(
|
||||||
|
repo domain.CourseRepository,
|
||||||
|
log *slog.Logger,
|
||||||
|
) UpdateCourseDescriptionHandler {
|
||||||
|
h := updateCourseDescriptionHandler{
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorator.ApplyCommandDecorators(h, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h updateCourseDescriptionHandler) Handle(ctx context.Context, cmd UpdateCourseDescription) error {
|
||||||
|
return h.repo.UpdateCourseDescription(ctx, cmd.ID, cmd.Description)
|
||||||
|
}
|
||||||
@ -16,15 +16,18 @@ type GetCourse struct {
|
|||||||
type GetCourseHandler decorator.QueryHandler[GetCourse, domain.Course]
|
type GetCourseHandler decorator.QueryHandler[GetCourse, domain.Course]
|
||||||
|
|
||||||
type getCourseHandler struct {
|
type getCourseHandler struct {
|
||||||
repo domain.CourseRepository
|
repo domain.CourseRepository
|
||||||
|
mapper domain.CourseMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGetCourseHandler(
|
func NewGetCourseHandler(
|
||||||
repo domain.CourseRepository,
|
repo domain.CourseRepository,
|
||||||
|
mapper domain.CourseMapper,
|
||||||
log *slog.Logger,
|
log *slog.Logger,
|
||||||
) GetCourseHandler {
|
) GetCourseHandler {
|
||||||
h := getCourseHandler{
|
h := getCourseHandler{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
mapper: mapper,
|
||||||
}
|
}
|
||||||
return decorator.AddQueryDecorators(h, log)
|
return decorator.AddQueryDecorators(h, log)
|
||||||
}
|
}
|
||||||
@ -35,5 +38,8 @@ func (h getCourseHandler) Handle(ctx context.Context, query GetCourse) (domain.C
|
|||||||
return domain.Course{}, fmt.Errorf("getting course: %w", err)
|
return domain.Course{}, fmt.Errorf("getting course: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
course.LearningType = h.mapper.LearningTypeNameByID(course.LearningTypeID)
|
||||||
|
course.Thematic = h.mapper.CourseThematicNameByID(course.ThematicID)
|
||||||
|
|
||||||
return course, nil
|
return course, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,37 +17,64 @@ type ListCourse struct {
|
|||||||
OrganizationID string
|
OrganizationID string
|
||||||
Keyword string
|
Keyword string
|
||||||
|
|
||||||
Limit int
|
Limit int
|
||||||
Offset int
|
NextPageToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListCourseHandler decorator.QueryHandler[ListCourse, []domain.Course]
|
type ListCourseHandler decorator.QueryHandler[ListCourse, domain.ListCoursesResult]
|
||||||
|
|
||||||
type listCourseHandler struct {
|
type listCourseHandler struct {
|
||||||
repo domain.CourseRepository
|
repo domain.CourseRepository
|
||||||
|
mapper domain.CourseMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListCourseHandler(
|
func NewListCourseHandler(
|
||||||
repo domain.CourseRepository,
|
repo domain.CourseRepository,
|
||||||
|
mapper domain.CourseMapper,
|
||||||
log *slog.Logger,
|
log *slog.Logger,
|
||||||
) ListCourseHandler {
|
) ListCourseHandler {
|
||||||
h := listCourseHandler{
|
h := listCourseHandler{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
mapper: mapper,
|
||||||
}
|
}
|
||||||
return decorator.AddQueryDecorators(h, log)
|
return decorator.AddQueryDecorators(h, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) ([]domain.Course, error) {
|
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) {
|
||||||
courses, err := h.repo.List(ctx, domain.ListCoursesParams{
|
out.NextPageToken = query.NextPageToken
|
||||||
CourseThematic: query.CourseThematic,
|
drainFull := query.Limit == 0
|
||||||
LearningType: query.LearningType,
|
if !drainFull {
|
||||||
OrganizationID: query.OrganizationID,
|
out.Courses = make([]domain.Course, 0, query.Limit)
|
||||||
Limit: query.Limit,
|
}
|
||||||
Offset: query.Offset,
|
for {
|
||||||
})
|
result, err := h.repo.List(ctx, domain.ListCoursesParams{
|
||||||
if err != nil {
|
CourseThematic: query.CourseThematic,
|
||||||
return nil, fmt.Errorf("listing courses: %w", err)
|
LearningType: query.LearningType,
|
||||||
|
OrganizationID: query.OrganizationID,
|
||||||
|
Limit: query.Limit,
|
||||||
|
|
||||||
|
NextPageToken: out.NextPageToken,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("listing courses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Courses = xslices.Map(result.Courses, func(in domain.Course) domain.Course {
|
||||||
|
in.LearningType = h.mapper.LearningTypeNameByID(in.LearningTypeID)
|
||||||
|
in.Thematic = h.mapper.CourseThematicNameByID(in.ThematicID)
|
||||||
|
|
||||||
|
return in
|
||||||
|
})
|
||||||
|
|
||||||
|
out.Courses = append(out.Courses, result.Courses...)
|
||||||
|
out.NextPageToken = result.NextPageToken
|
||||||
|
|
||||||
|
if drainFull && len(result.Courses) > 0 && result.NextPageToken != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return courses, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
62
internal/kurious/app/query/listcoursethematics.go
Normal file
62
internal/kurious/app/query/listcoursethematics.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListCourseThematics struct {
|
||||||
|
LearningTypeID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CourseThematic struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListCourseThematicsResult struct {
|
||||||
|
CourseThematics []CourseThematic
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListCourseThematicsHandler decorator.QueryHandler[ListCourseThematics, ListCourseThematicsResult]
|
||||||
|
|
||||||
|
type listCourseThematicsHandler struct {
|
||||||
|
repo domain.CourseRepository
|
||||||
|
mapper domain.CourseMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListCourseThematicsHandler(
|
||||||
|
repo domain.CourseRepository,
|
||||||
|
mapper domain.CourseMapper,
|
||||||
|
log *slog.Logger,
|
||||||
|
) ListCourseThematicsHandler {
|
||||||
|
h := listCourseThematicsHandler{
|
||||||
|
repo: repo,
|
||||||
|
mapper: mapper,
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorator.AddQueryDecorators(h, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h listCourseThematicsHandler) Handle(ctx context.Context, query ListCourseThematics) (out ListCourseThematicsResult, err error) {
|
||||||
|
result, err := h.repo.ListCourseThematics(ctx, domain.ListCourseThematicsParams{
|
||||||
|
LearningTypeID: query.LearningTypeID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("listing course thematics from repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.CourseThematics = make([]CourseThematic, 0, len(result.CourseThematicIDs))
|
||||||
|
for _, ct := range result.CourseThematicIDs {
|
||||||
|
var item CourseThematic
|
||||||
|
item.ID = ct
|
||||||
|
item.Name = h.mapper.CourseThematicNameByID(ct)
|
||||||
|
out.CourseThematics = append(out.CourseThematics, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
58
internal/kurious/app/query/listlearningtypes.go
Normal file
58
internal/kurious/app/query/listlearningtypes.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListLearningTypes struct{}
|
||||||
|
|
||||||
|
type LearningType struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLearningTypesResult struct {
|
||||||
|
LearningTypes []LearningType
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLearningTypesHandler decorator.QueryHandler[ListLearningTypes, ListLearningTypesResult]
|
||||||
|
|
||||||
|
type listLearningTypesHandler struct {
|
||||||
|
repo domain.CourseRepository
|
||||||
|
mapper domain.CourseMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListLearningTypesHandler(
|
||||||
|
repo domain.CourseRepository,
|
||||||
|
mapper domain.CourseMapper,
|
||||||
|
log *slog.Logger,
|
||||||
|
) ListLearningTypesHandler {
|
||||||
|
h := listLearningTypesHandler{
|
||||||
|
repo: repo,
|
||||||
|
mapper: mapper,
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorator.AddQueryDecorators(h, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h listLearningTypesHandler) Handle(ctx context.Context, query ListLearningTypes) (out ListLearningTypesResult, err error) {
|
||||||
|
result, err := h.repo.ListLearningTypes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("listing learning types from repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.LearningTypes = make([]LearningType, 0, len(result.LearningTypeIDs))
|
||||||
|
for _, lt := range result.LearningTypeIDs {
|
||||||
|
var item LearningType
|
||||||
|
item.ID = lt
|
||||||
|
item.Name = h.mapper.LearningTypeNameByID(lt)
|
||||||
|
out.LearningTypes = append(out.LearningTypes, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@ -27,9 +27,13 @@ type Course struct {
|
|||||||
// FullPrice is a course full price without discount.
|
// FullPrice is a course full price without discount.
|
||||||
FullPrice float64
|
FullPrice float64
|
||||||
// Discount for the course.
|
// Discount for the course.
|
||||||
Discount float64
|
Discount float64
|
||||||
Thematic string
|
|
||||||
LearningType string
|
Thematic string
|
||||||
|
ThematicID string
|
||||||
|
|
||||||
|
LearningType string
|
||||||
|
LearningTypeID string
|
||||||
|
|
||||||
// Duration for the course. It will be splitted in values like:
|
// Duration for the course. It will be splitted in values like:
|
||||||
// full month / full day / full hour.
|
// full month / full day / full hour.
|
||||||
|
|||||||
6
internal/kurious/domain/mapper.go
Normal file
6
internal/kurious/domain/mapper.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
type CourseMapper interface {
|
||||||
|
CourseThematicNameByID(string) string
|
||||||
|
LearningTypeNameByID(string) string
|
||||||
|
}
|
||||||
@ -14,7 +14,6 @@ type ListCoursesParams struct {
|
|||||||
|
|
||||||
NextPageToken string
|
NextPageToken string
|
||||||
Limit int
|
Limit int
|
||||||
Offset int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateCourseParams struct {
|
type CreateCourseParams struct {
|
||||||
@ -40,10 +39,24 @@ type ListCoursesResult struct {
|
|||||||
NextPageToken string
|
NextPageToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListLearningTypeResult struct {
|
||||||
|
LearningTypeIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListCourseThematicsParams struct {
|
||||||
|
LearningTypeID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListCourseThematicsResult struct {
|
||||||
|
CourseThematicIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
//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(ctx context.Context, params ListCoursesParams) (ListCoursesResult, error)
|
List(context.Context, ListCoursesParams) (ListCoursesResult, error)
|
||||||
|
ListLearningTypes(context.Context) (ListLearningTypeResult, error)
|
||||||
|
ListCourseThematics(context.Context, ListCourseThematicsParams) (ListCourseThematicsResult, 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)
|
||||||
@ -57,6 +70,9 @@ type CourseRepository interface {
|
|||||||
Create(context.Context, CreateCourseParams) (Course, error)
|
Create(context.Context, CreateCourseParams) (Course, error)
|
||||||
// Delete course by id.
|
// Delete course by id.
|
||||||
Delete(ctx context.Context, id string) error
|
Delete(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
// UpdateCourseDescription is a temporary method to udpate description field
|
||||||
|
UpdateCourseDescription(ctx context.Context, id string, description string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateOrganizationParams struct {
|
type CreateOrganizationParams struct {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import (
|
|||||||
"git.loyso.art/frx/kurious/internal/common/generator"
|
"git.loyso.art/frx/kurious/internal/common/generator"
|
||||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
"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/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"
|
||||||
@ -126,7 +126,7 @@ func (h *syncSravniHandler) Handle(ctx context.Context) (err error) {
|
|||||||
return fmt.Errorf("loading educational products: %w", err)
|
return fmt.Errorf("loading educational products: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
xslice.ForEach(buffer, func(c sravni.Course) {
|
xslices.ForEach(buffer, func(c sravni.Course) {
|
||||||
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)
|
||||||
@ -155,7 +155,7 @@ func (h *syncSravniHandler) Handle(ctx context.Context) (err error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
xslice.ForEach(courses, func(c sravni.Course) {
|
xslices.ForEach(courses, func(c sravni.Course) {
|
||||||
h.knownExternalIDs[c.ID] = struct{}{}
|
h.knownExternalIDs[c.ID] = struct{}{}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learnin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *syncSravniHandler) filterByCache(courses []sravni.Course) (toInsert []sravni.Course) {
|
func (h *syncSravniHandler) filterByCache(courses []sravni.Course) (toInsert []sravni.Course) {
|
||||||
toCut := xslice.FilterInplace(courses, xslice.Not(h.isCached))
|
toCut := xslices.FilterInplace(courses, xslices.Not(h.isCached))
|
||||||
return courses[:toCut]
|
return courses[:toCut]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ func (h *syncSravniHandler) isCached(course sravni.Course) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *syncSravniHandler) insertValues(ctx context.Context, courses []sravni.Course) error {
|
func (h *syncSravniHandler) insertValues(ctx context.Context, courses []sravni.Course) error {
|
||||||
courseParams := xslice.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,
|
||||||
})
|
})
|
||||||
@ -240,14 +240,16 @@ func (h *syncSravniHandler) fillCaches(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
courses, err := h.svc.Queries.ListCourses.Handle(ctx, query.ListCourse{})
|
result, err := h.svc.Queries.ListCourses.Handle(ctx, query.ListCourse{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listing courses: %w", err)
|
return fmt.Errorf("listing courses: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
courses := result.Courses
|
||||||
|
|
||||||
h.knownExternalIDs = make(map[string]struct{}, len(courses))
|
h.knownExternalIDs = make(map[string]struct{}, len(courses))
|
||||||
|
|
||||||
xslice.ForEach(courses, func(c domain.Course) {
|
xslices.ForEach(courses, func(c domain.Course) {
|
||||||
if !c.ExternalID.Valid() {
|
if !c.ExternalID.Valid() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +1,36 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/app"
|
"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/service"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
type courseServer struct {
|
type courseServer struct {
|
||||||
app app.Application
|
app service.Application
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type pagination struct {
|
type pagination struct {
|
||||||
page int
|
nextPageToken string
|
||||||
perPage int
|
perPage int
|
||||||
}
|
|
||||||
|
|
||||||
func (p pagination) LimitOffset() (limit, offset int) {
|
|
||||||
if p.page < 0 {
|
|
||||||
p.page = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.perPage <= 0 {
|
|
||||||
p.perPage = defaultPerPage
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.perPage, p.page * p.perPage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
|
out.nextPageToken = query.Get("next")
|
||||||
if query.Has("page") {
|
|
||||||
out.page, err = strconv.Atoi(query.Get("page"))
|
|
||||||
if err != nil {
|
|
||||||
return out, errors.NewValidationError("page", "bad page value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.Has("per_page") {
|
if query.Has("per_page") {
|
||||||
out.perPage, err = strconv.Atoi(query.Get("per_page"))
|
out.perPage, err = strconv.Atoi(query.Get("per_page"))
|
||||||
@ -57,15 +44,20 @@ func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
LearningTypePathParam = "learning_type"
|
||||||
|
ThematicTypePathParam = "thematic_type"
|
||||||
|
)
|
||||||
|
|
||||||
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
||||||
out.pagination, err = parsePaginationFromQuery(r)
|
out.pagination, err = parsePaginationFromQuery(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
query := r.URL.Query()
|
vars := mux.Vars(r)
|
||||||
out.learningType = query.Get("category")
|
out.learningType = vars[LearningTypePathParam]
|
||||||
out.courseThematic = query.Get("type")
|
out.courseThematic = vars[ThematicTypePathParam]
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@ -77,40 +69,274 @@ type listCoursesParams struct {
|
|||||||
learningType string
|
learningType string
|
||||||
}
|
}
|
||||||
|
|
||||||
type templateCourse domain.Course
|
type baseInfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
func mapDomainCourseToTemplate(in ...domain.Course) []templateCourse {
|
type categoryInfo struct {
|
||||||
return xslice.Map(in, func(v domain.Course) templateCourse {
|
baseInfo
|
||||||
return templateCourse(v)
|
|
||||||
|
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))
|
||||||
|
categoryByID := make(map[string]baseInfo, len(in))
|
||||||
|
|
||||||
|
xslices.ForEach(in, func(c domain.Course) {
|
||||||
|
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], c)
|
||||||
|
if _, ok := subcategoriesByCategories[c.LearningTypeID]; !ok {
|
||||||
|
subcategoriesByCategories[c.LearningTypeID] = map[string]struct{}{}
|
||||||
|
}
|
||||||
|
subcategoriesByCategories[c.LearningTypeID][c.ThematicID] = struct{}{}
|
||||||
|
if _, ok := categoryByID[c.LearningTypeID]; !ok {
|
||||||
|
categoryByID[c.LearningTypeID] = baseInfo{
|
||||||
|
ID: c.LearningTypeID,
|
||||||
|
Name: c.LearningType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := categoryByID[c.ThematicID]; !ok {
|
||||||
|
categoryByID[c.ThematicID] = baseInfo{
|
||||||
|
ID: c.ThematicID,
|
||||||
|
Name: c.Thematic,
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var out listCoursesTemplateParams
|
||||||
|
for category, subcategoryMap := range subcategoriesByCategories {
|
||||||
|
outCategory := categoryInfo{
|
||||||
|
baseInfo: categoryByID[category],
|
||||||
|
}
|
||||||
|
|
||||||
|
for subcategory := range subcategoryMap {
|
||||||
|
outSubCategory := subcategoryInfo{
|
||||||
|
baseInfo: categoryByID[subcategory],
|
||||||
|
Courses: coursesBySubcategory[subcategory],
|
||||||
|
}
|
||||||
|
|
||||||
|
outCategory.Subcategories = append(outCategory.Subcategories, outSubCategory)
|
||||||
|
}
|
||||||
|
sort.Slice(outCategory.Subcategories, func(i, j int) bool {
|
||||||
|
return outCategory.Subcategories[i].ID < outCategory.Subcategories[j].ID
|
||||||
|
})
|
||||||
|
out.Categories = append(out.Categories, outCategory)
|
||||||
|
}
|
||||||
|
sort.Slice(out.Categories, func(i, j int) bool {
|
||||||
|
return out.Categories[i].ID < out.Categories[j].ID
|
||||||
|
})
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
|
func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
params, err := parseListCoursesParams(r)
|
params, err := parseListCoursesParams(r)
|
||||||
if err != nil {
|
if handleError(ctx, err, w, c.log, "unable to parse list courses params") {
|
||||||
handleError(ctx, err, w, c.log, "unable to parse list courses params")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limit, offset := params.LimitOffset()
|
result, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||||
|
|
||||||
courses, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
|
||||||
CourseThematic: params.courseThematic,
|
CourseThematic: params.courseThematic,
|
||||||
LearningType: params.learningType,
|
LearningType: params.learningType,
|
||||||
Limit: limit,
|
Limit: params.perPage,
|
||||||
Offset: offset,
|
NextPageToken: params.nextPageToken,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||||
handleError(ctx, err, w, c.log, "unable to list courses")
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
courses := result.Courses
|
||||||
templateCourses := mapDomainCourseToTemplate(courses...)
|
templateCourses := mapDomainCourseToTemplate(courses...)
|
||||||
|
templateCourses.NextPageToken = result.NextPageToken
|
||||||
|
|
||||||
err = template.Must(template.ParseFiles("templates/list.tmpl")).Execute(w, templateCourses)
|
learningTypeList, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templateCourses.AvailableLearningTypes = xslices.Map(learningTypeList.LearningTypes, func(in query.LearningType) IDNamePair {
|
||||||
|
if in.ID == params.learningType {
|
||||||
|
templateCourses.LearningTypeName = in.Name
|
||||||
|
}
|
||||||
|
return IDNamePair{
|
||||||
|
ID: in.ID,
|
||||||
|
Name: in.Name,
|
||||||
|
IsActive: in.ID == params.learningType,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
templateCourses.ActiveLearningType = params.learningType
|
||||||
|
templateCourses.ActiveCourseThematic = params.courseThematic
|
||||||
|
|
||||||
|
if params.learningType != "" {
|
||||||
|
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
|
||||||
|
LearningTypeID: params.learningType,
|
||||||
|
})
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templateCourses.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) IDNamePair {
|
||||||
|
if in.ID == params.courseThematic {
|
||||||
|
templateCourses.CourseThematicName = in.Name
|
||||||
|
}
|
||||||
|
return IDNamePair{
|
||||||
|
ID: in.ID,
|
||||||
|
Name: in.Name,
|
||||||
|
IsActive: in.ID == params.courseThematic,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "courses", templateCourses)
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c courseServer) Get(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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
handleError(ctx, err, w, c.log, "unable to execute template")
|
xcontext.LogWithWarnError(ctx, c.log, err, "unable to write a message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c courseServer) GetShort(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
id := mux.Vars(r)["course_id"]
|
||||||
|
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
||||||
|
ID: id,
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "edit_description", course)
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c courseServer) UpdateCourseDescription(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type requestModel struct {
|
||||||
|
ID string `json:"-"`
|
||||||
|
Text string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
var req requestModel
|
||||||
|
req.ID = mux.Vars(r)["course_id"]
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to read body") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.app.Commands.UpdateCourseDescription.Handle(ctx, command.UpdateCourseDescription{
|
||||||
|
ID: req.ID,
|
||||||
|
Description: req.Text,
|
||||||
|
})
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to update course description") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
course, err := c.app.Queries.GetCourse.Handle(ctx, query.GetCourse{
|
||||||
|
ID: req.ID,
|
||||||
|
})
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to get course") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "course_info", course)
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c courseServer) UdpateDescription(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type requestModel struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
var req requestModel
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to read body") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.app.Commands.UpdateCourseDescription.Handle(ctx, command.UpdateCourseDescription{
|
||||||
|
ID: req.ID,
|
||||||
|
Description: req.Text,
|
||||||
|
})
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to update course description") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
internal/kurious/ports/http/course_test.go
Normal file
60
internal/kurious/ports/http/course_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
139
internal/kurious/ports/http/coursev2.go
Normal file
139
internal/kurious/ports/http/coursev2.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
192
internal/kurious/ports/http/listtemplate.go
Normal file
192
internal/kurious/ports/http/listtemplate.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
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}}`
|
||||||
@ -8,30 +8,32 @@ import (
|
|||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/app"
|
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultPerPage = 50
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
app app.Application
|
app service.Application
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(app app.Application) Server {
|
func NewServer(app service.Application, log *slog.Logger) Server {
|
||||||
return Server{}
|
return Server{
|
||||||
}
|
app: app,
|
||||||
|
log: log,
|
||||||
func (s Server) Courses() courseServer {
|
|
||||||
return courseServer{
|
|
||||||
app: s.app,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slog.Logger, msg string) {
|
func (s Server) Courses() courseServer {
|
||||||
|
return courseServer(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) CoursesByTempl() courseTemplServer {
|
||||||
|
return courseTemplServer(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slog.Logger, msg string) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorString string
|
var errorString string
|
||||||
@ -52,4 +54,6 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
|
|||||||
xcontext.LogWithWarnError(ctx, log, err, msg, slog.Int("status_code", code), slog.String("response", errorString))
|
xcontext.LogWithWarnError(ctx, log, err, msg, slog.Int("status_code", code), slog.String("response", errorString))
|
||||||
|
|
||||||
http.Error(w, errorString, code)
|
http.Error(w, errorString, code)
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
22
internal/kurious/ports/http/templ/common.templ
Normal file
22
internal/kurious/ports/http/templ/common.templ
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package templ
|
||||||
|
|
||||||
|
templ button(title string, attributes templ.Attributes) {
|
||||||
|
<button class="button" { attributes... }>{ title }</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ buttonRedirect(id, title string, linkTo string) {
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
id={ "origin-link-" + id }
|
||||||
|
>
|
||||||
|
{ title }
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@onclickRedirect("origin-link-" + id, linkTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
script onclickRedirect(id, to string) {
|
||||||
|
document.getElementById(id).onclick = () => {
|
||||||
|
location.href = to
|
||||||
|
}
|
||||||
|
}
|
||||||
116
internal/kurious/ports/http/templ/common_templ.go
Normal file
116
internal/kurious/ports/http/templ/common_templ.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.513
|
||||||
|
package templ
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
func button(title string, attributes templ.Attributes) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"button\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, attributes)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/common.templ`, Line: 3, Col: 49}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buttonRedirect(id, title string, linkTo string) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var3 == nil {
|
||||||
|
templ_7745c5c3_Var3 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"button\" id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("origin-link-" + id))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/templ/common.templ`, Line: 11, Col: 8}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = onclickRedirect("origin-link-"+id, linkTo).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func onclickRedirect(id, to string) templ.ComponentScript {
|
||||||
|
return templ.ComponentScript{
|
||||||
|
Name: `__templ_onclickRedirect_47ae`,
|
||||||
|
Function: `function __templ_onclickRedirect_47ae(id, to){document.getElementById(id).onclick = () => {
|
||||||
|
location.href = to
|
||||||
|
}}`,
|
||||||
|
Call: templ.SafeScript(`__templ_onclickRedirect_47ae`, id, to),
|
||||||
|
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_47ae`, id, to),
|
||||||
|
}
|
||||||
|
}
|
||||||
62
internal/kurious/ports/http/templ/header.templ
Normal file
62
internal/kurious/ports/http/templ/header.templ
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
182
internal/kurious/ports/http/templ/header_templ.go
Normal file
182
internal/kurious/ports/http/templ/header_templ.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
209
internal/kurious/ports/http/templ/list.templ
Normal file
209
internal/kurious/ports/http/templ/list.templ
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
714
internal/kurious/ports/http/templ/list_templ.go
Normal file
714
internal/kurious/ports/http/templ/list_templ.go
Normal file
@ -0,0 +1,714 @@
|
|||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
95
internal/kurious/ports/http/templ/vars.go
Normal file
95
internal/kurious/ports/http/templ/vars.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package templ
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCompactedValue(value int) string {
|
||||||
|
var (
|
||||||
|
myValue float64
|
||||||
|
dim string
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case value/1e6 > 0:
|
||||||
|
cutted := value / 1e3
|
||||||
|
myValue, dim = float64(cutted)/1e3, "m"
|
||||||
|
case value/1e3 > 0:
|
||||||
|
myValue, dim = float64(value/1e3), "k"
|
||||||
|
default:
|
||||||
|
myValue, dim = float64(value), ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSuffix(strconv.FormatFloat(myValue, 'f', 3, 32), ".000") + dim
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeNewStats(courses, clients, categories int) stats {
|
||||||
|
return stats{
|
||||||
|
CoursesCount: getCompactedValue(courses),
|
||||||
|
ClientsCount: getCompactedValue(clients),
|
||||||
|
CategoriesCount: getCompactedValue(categories),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type stats struct {
|
||||||
|
CoursesCount string
|
||||||
|
ClientsCount string
|
||||||
|
CategoriesCount string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Category) Empty() bool {
|
||||||
|
return c == (Category{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type BreadcrumbsParams struct {
|
||||||
|
ActiveLearningType Category
|
||||||
|
ActiveCourseThematic Category
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterFormParams struct {
|
||||||
|
BreadcrumbsParams
|
||||||
|
|
||||||
|
AvailableLearningTypes []Category
|
||||||
|
AvailableCourseThematics []Category
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmpty(s string) bool {
|
||||||
|
return s == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type CourseInfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
FullPrice int
|
||||||
|
ImageLink string
|
||||||
|
OriginLink string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryBaseInfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryContainer struct {
|
||||||
|
CategoryBaseInfo
|
||||||
|
|
||||||
|
Subcategories []SubcategoryContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubcategoryContainer struct {
|
||||||
|
CategoryBaseInfo
|
||||||
|
|
||||||
|
Courses []CourseInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListCoursesParams struct {
|
||||||
|
FilterForm FilterFormParams
|
||||||
|
|
||||||
|
Categories []CategoryContainer
|
||||||
|
}
|
||||||
57
internal/kurious/ports/http/templ/vars_test.go
Normal file
57
internal/kurious/ports/http/templ/vars_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
73
internal/kurious/ports/http/templates/common.tmpl
Normal file
73
internal/kurious/ports/http/templates/common.tmpl
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{{ 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 }}
|
||||||
|
|
||||||
81
internal/kurious/ports/http/templates/get.tmpl
Normal file
81
internal/kurious/ports/http/templates/get.tmpl
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
{{ define "course_info" }}
|
||||||
|
<article class="column is-one-quarter" hx-target="this" hx-swap="outerHTML">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-image">
|
||||||
|
<figure class="image">
|
||||||
|
<img src="{{ .ImageLink }}">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-5" onclick="location.href='{{ .OriginLink }}'">{{ .Name }}</p>
|
||||||
|
<p class="subtitle is-8">{{ .Description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
{{ if .FullPrice }}
|
||||||
|
<p>{{ .FullPrice }} rub.</p>
|
||||||
|
{{ else }}
|
||||||
|
<p>Бесплатно</p>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button" onclick="location.href='{{ .OriginLink }}'">
|
||||||
|
Show Course
|
||||||
|
</button>
|
||||||
|
<button class="button" hx-get="/courses/{{ .ID }}/editdesc">
|
||||||
|
Edit description
|
||||||
|
</button>
|
||||||
|
<!-- <button class="button" hx-get="/courses/{{ .ID }}/" hx-target="#course-info" hx-swap="innerHTML">
|
||||||
|
View course
|
||||||
|
</button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "edit_description" }}
|
||||||
|
<form
|
||||||
|
hx-ext="json-enc"
|
||||||
|
hx-put="/courses/{{ .ID }}/description"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<fieldset disabled>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Name</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" placeholder="Text input" value="{{ .Name }}">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<div class="field-label is-normal">
|
||||||
|
<label class="label">Description</label>
|
||||||
|
</div>
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<textarea class="textarea" name="description" placeholder="Description">{{ .Description }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Full price: {{ .FullPrice }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-primary is-link" hx-include="[name='description']">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-light" hx-get="/courses/{{ .ID }}/short">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
@ -1,86 +1,185 @@
|
|||||||
{{define "courses"}}
|
{{define "courses"}}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
{{ template "html_head" . }}
|
||||||
<title>Courses</title>
|
|
||||||
<style>
|
<body>
|
||||||
body {
|
{{ template "header" . }}
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
margin: 0;
|
<nav class="level">
|
||||||
padding: 0;
|
<div class="level-item has-text-centered">
|
||||||
}
|
<div>
|
||||||
header {
|
<p class="heading">Courses</p>
|
||||||
background-color: #333;
|
<p class="title">10k</p>
|
||||||
color: white;
|
</div>
|
||||||
padding: 10px;
|
</div>
|
||||||
}
|
<div class="level-item has-text-centered">
|
||||||
header h1 {
|
<div>
|
||||||
margin: 0;
|
<p class="heading">Clients</p>
|
||||||
}
|
<p class="title">1m</p>
|
||||||
nav ul {
|
</div>
|
||||||
list-style-type: none;
|
</div>
|
||||||
margin: 0;
|
<div class="level-item has-text-centered">
|
||||||
padding: 0;
|
<div>
|
||||||
}
|
<p class="heading">Categories</p>
|
||||||
nav li {
|
<p class="title">1,024</p>
|
||||||
display: inline;
|
</div>
|
||||||
margin-right: 10px;
|
</div>
|
||||||
}
|
<div class="level-item has-text-centered">
|
||||||
nav a {
|
<div>
|
||||||
color: white;
|
<p class="heading">Likes</p>
|
||||||
text-decoration: none;
|
<p class="title">Over 9m</p>
|
||||||
}
|
</div>
|
||||||
h1, h2, h3 {
|
</div>
|
||||||
margin-top: 0;
|
</nav>
|
||||||
}
|
|
||||||
p {
|
<div class="section">
|
||||||
margin-bottom: 10px;
|
<div class="container block">
|
||||||
}
|
<nav class="breadcrumb" aria-label="breadcrumbs" itemprop="breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">
|
||||||
.course-plate {
|
|
||||||
background-color: #f2f2f2;
|
<ul>
|
||||||
border: 1px solid #ddd;
|
<li>
|
||||||
border-radius: 5px;
|
{{ if .LearningTypeName }}
|
||||||
padding: 10px;
|
<a href="/courses" itemprop="url">
|
||||||
margin-bottom: 10px;
|
<span itemprop="title">
|
||||||
text-align: center;
|
Курсы
|
||||||
}
|
</span>
|
||||||
.course-plate a {
|
</a>
|
||||||
color: #333;
|
{{ else }}
|
||||||
text-decoration: none;
|
<a>
|
||||||
}
|
<span itemprop="title" itemprop="url">
|
||||||
.course-plate a:hover {
|
Курсы
|
||||||
text-decoration: underline;
|
</span>
|
||||||
}
|
</a>
|
||||||
</style>
|
{{ end }}
|
||||||
</head>
|
</li>
|
||||||
<body>
|
|
||||||
<header>
|
{{ if .LearningTypeName }}
|
||||||
<h1>My Product</h1>
|
<li>
|
||||||
<nav>
|
{{ if .CourseThematicName }}
|
||||||
<ul>
|
<a href="/courses/{{.ActiveLearningType}}" itemprop="url">
|
||||||
<li><a href="/">Main page</a></li>
|
<span itemprop="title">
|
||||||
<li><a href="/about">About us</a></li>
|
{{ .LearningTypeName }}
|
||||||
<li><a href="/help">Help</a></li>
|
</span>
|
||||||
</ul>
|
</a>
|
||||||
</nav>
|
{{ else }}
|
||||||
</header>
|
<a>
|
||||||
<h1>Courses</h1>
|
<span itemprop="title" itemprop="url">
|
||||||
{{range $category, $courses := .Courses}}
|
{{ .LearningTypeName }}
|
||||||
<h2>{{$category}}</h2>
|
</span>
|
||||||
<p>{{$category.Description}}</p>
|
</a>
|
||||||
{{range $course := $courses}}
|
{{ end }}
|
||||||
<div class="course-plate">
|
</li>
|
||||||
<h3><a href="/courses/{{$course.ID}}">{{$course.Name}}</a></h3>
|
{{ end }}
|
||||||
<p>{{$course.Description}}</p>
|
|
||||||
<p>Full price: {{$course.FullPrice}}</p>
|
{{ if .CourseThematicName }}
|
||||||
<p>Discount: {{$course.Discount}}</p>
|
<li>
|
||||||
<p>Thematic: {{$course.Thematic}}</p>
|
<a>
|
||||||
<p>Learning type: {{$course.LearningType}}</p>
|
<span itemprop="title" itemprop="url">
|
||||||
<p>Duration: {{$course.Duration}}</p>
|
{{ .CourseThematicName }}
|
||||||
<p>Starts at: {{$course.StartsAt}}</p>
|
</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>
|
||||||
{{end}}
|
|
||||||
{{end}}
|
<div class="subtitle is-6">
|
||||||
</body>
|
Some description about the learning category {{ $category.Description }}
|
||||||
</html>
|
</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}}
|
{{end}}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"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"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApplicationConfig struct {
|
type ApplicationConfig struct {
|
||||||
@ -25,7 +26,7 @@ type Application struct {
|
|||||||
closers []io.Closer
|
closers []io.Closer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApplication(ctx context.Context, cfg ApplicationConfig) (Application, error) {
|
func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.CourseMapper) (Application, error) {
|
||||||
log := config.NewSLogger(cfg.LogConfig)
|
log := config.NewSLogger(cfg.LogConfig)
|
||||||
ydbConnection, err := adapters.NewYDBConnection(ctx, cfg.YDB, log.With(slog.String("db", "ydb")))
|
ydbConnection, err := adapters.NewYDBConnection(ctx, cfg.YDB, log.With(slog.String("db", "ydb")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -36,13 +37,16 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig) (Application, er
|
|||||||
|
|
||||||
application := app.Application{
|
application := app.Application{
|
||||||
Commands: app.Commands{
|
Commands: app.Commands{
|
||||||
InsertCourses: command.NewCreateCoursesHandler(courseadapter, log),
|
InsertCourses: command.NewCreateCoursesHandler(courseadapter, log),
|
||||||
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),
|
||||||
},
|
},
|
||||||
Queries: app.Queries{
|
Queries: app.Queries{
|
||||||
GetCourse: query.NewGetCourseHandler(courseadapter, log),
|
ListCourses: query.NewListCourseHandler(courseadapter, mapper, log),
|
||||||
ListCourses: query.NewListCourseHandler(courseadapter, log),
|
ListLearningTypes: query.NewListLearningTypesHandler(courseadapter, mapper, log),
|
||||||
|
ListCourseThematics: query.NewListCourseThematicsHandler(courseadapter, mapper, log),
|
||||||
|
GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
package slices
|
|
||||||
|
|
||||||
// Map slice from one type to another one.
|
|
||||||
func Map[S any, E any](s []S, f func(S) E) []E {
|
|
||||||
out := make([]E, len(s))
|
|
||||||
for i := range s {
|
|
||||||
out[i] = f(s[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user