From 48f5d80f7a38b8bbcf695df51a3a455cdf953fd8 Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Fri, 5 Jan 2024 23:03:15 +0300 Subject: [PATCH] add more style --- assets/kurious/static/style.css | 6 + cmd/kuriweb/main.go | 13 +- internal/common/config/http.go | 1 + .../kurious/adapters/ydb_course_repository.go | 107 +++++++------ internal/kurious/ports/http/course.go | 76 ++++++++- internal/kurious/ports/http/listtemplate.go | 45 +++++- .../kurious/ports/http/templates/common.tmpl | 68 ++++++++ .../kurious/ports/http/templates/get.tmpl | 76 +++++++++ .../kurious/ports/http/templates/list.tmpl | 151 ++++++++---------- pkg/slices/map.go | 6 + 10 files changed, 415 insertions(+), 134 deletions(-) create mode 100644 assets/kurious/static/style.css create mode 100644 internal/kurious/ports/http/templates/common.tmpl create mode 100644 internal/kurious/ports/http/templates/get.tmpl diff --git a/assets/kurious/static/style.css b/assets/kurious/static/style.css new file mode 100644 index 0000000..f477431 --- /dev/null +++ b/assets/kurious/static/style.css @@ -0,0 +1,6 @@ +.btn.btn-primary { + color: white; + background-color: black; + border: none; + border-radius: 4px; +} diff --git a/cmd/kuriweb/main.go b/cmd/kuriweb/main.go index 18d5a8f..0b3191b 100644 --- a/cmd/kuriweb/main.go +++ b/cmd/kuriweb/main.go @@ -100,7 +100,18 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server router.HandleFunc("/updatedesc", coursesAPI.UdpateDescription).Methods(http.MethodPost) coursesRouter := router.PathPrefix("/courses").Subrouter() coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet) - coursesRouter.HandleFunc("/{course_id}", coursesAPI.Get).Methods(http.MethodGet) + + courseRouter := coursesRouter.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) + + if cfg.MountLive { + fs := http.FileServer(http.Dir("./assets/kurious/static/")) + router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs)) + } return &http.Server{ Addr: cfg.ListenAddr, diff --git a/internal/common/config/http.go b/internal/common/config/http.go index 680a634..ad36b8e 100644 --- a/internal/common/config/http.go +++ b/internal/common/config/http.go @@ -2,4 +2,5 @@ package config type HTTP struct { ListenAddr string `json:"listen_addr"` + MountLive bool `json:"mount_live"` } diff --git a/internal/kurious/adapters/ydb_course_repository.go b/internal/kurious/adapters/ydb_course_repository.go index 765df71..516fa00 100644 --- a/internal/kurious/adapters/ydb_course_repository.go +++ b/internal/kurious/adapters/ydb_course_repository.go @@ -59,7 +59,7 @@ type YDBConnection struct { } 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) { case config.YCAuthIAMToken: opts = append(opts, ydb.WithAccessTokenCredentials(auth.Token)) @@ -69,6 +69,10 @@ func NewYDBConnection(ctx context.Context, cfg config.YDB, log *slog.Logger) (*Y yc.WithServiceAccountKeyFileCredentials(auth.Path), ) } + opts = append(opts, + ydb.WithDialTimeout(time.Second*3), + ) + db, err := ydb.Open( ctx, cfg.DSN, @@ -117,7 +121,7 @@ func (r *ydbCourseRepository) List( qtParams := queryTemplateParams{ Fields: coursesFieldsStr, Table: "courses", - Suffix: "ORDER BY id\nLIMIT $limit", + Suffix: "ORDER BY learning_type,course_thematic,id\nLIMIT $limit", Declares: []queryTemplateDeclaration{ { Name: "limit", @@ -235,10 +239,36 @@ func (r *ydbCourseRepository) List( return result, err } -func (r *ydbCourseRepository) Get(ctx context.Context, id string) (course domain.Course, err error) { +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( table.BeginTx( table.WithOnlineReadOnly(), @@ -262,63 +292,48 @@ func (r *ydbCourseRepository) Get(ctx context.Context, id string) (course domain _, res, err := s.Execute( ctx, readTx, - ` - 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; - `, + querySelect, table.NewQueryParameters( table.ValueParam("$id", types.TextValue(id)), ), options.WithCollectStatsModeBasic(), ) if err != nil { - return fmt.Errorf("executing: %w", err) + return fmt.Errorf("executing query: %w", err) } - for res.NextResultSet(ctx) { - for res.NextRow() { - var cdb courseDB - _ = res.ScanNamed(cdb.getNamedValues()...) - courses = append(courses, mapCourseDB(cdb)) + if !res.NextResultSet(ctx) || !res.HasNextRow() { + return errors.ErrNotFound + } + + 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 { 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 }, - table.WithIdempotent()) - if err != nil { - return domain.Course{}, err - } - - if len(courses) == 0 { - return course, errors.ErrNotFound - } - - return courses[0], err + table.WithIdempotent(), + ) + return course, err } func (r *ydbCourseRepository) GetByExternalID(ctx context.Context, id string) (domain.Course, error) { diff --git a/internal/kurious/ports/http/course.go b/internal/kurious/ports/http/course.go index be63174..5e773d9 100644 --- a/internal/kurious/ports/http/course.go +++ b/internal/kurious/ports/http/course.go @@ -83,7 +83,8 @@ type subcategoryInfo struct { } type listCoursesTemplateParams struct { - Categories []categoryInfo + Categories []categoryInfo + NextPageToken string } func mapDomainCourseToTemplate(in ...domain.Course) listCoursesTemplateParams { @@ -146,8 +147,9 @@ func (c courseServer) List(w http.ResponseWriter, r *http.Request) { courses := result.Courses templateCourses := mapDomainCourseToTemplate(courses...) + templateCourses.NextPageToken = result.NextPageToken - err = listTemplateParsed.ExecuteTemplate(w, "courses", templateCourses) + err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "courses", templateCourses) if handleError(ctx, err, w, c.log, "unable to execute template") { return } @@ -177,6 +179,76 @@ func (c courseServer) Get(w http.ResponseWriter, r *http.Request) { } } +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:"text"` + } + + 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"` diff --git a/internal/kurious/ports/http/listtemplate.go b/internal/kurious/ports/http/listtemplate.go index 9c77103..4ab5556 100644 --- a/internal/kurious/ports/http/listtemplate.go +++ b/internal/kurious/ports/http/listtemplate.go @@ -1,6 +1,48 @@ package http -import "html/template" +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/xslice" +) + +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 := xslice.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"). @@ -111,6 +153,7 @@ const listTemplate = `{{define "courses"}} {{end}} {{end}} {{end}} + + + + + +{{ end }} + +{{ define "header" }} + + + +{{ end }} + +{{ define "footer" }} + + + +{{ end }} + diff --git a/internal/kurious/ports/http/templates/get.tmpl b/internal/kurious/ports/http/templates/get.tmpl new file mode 100644 index 0000000..70204e5 --- /dev/null +++ b/internal/kurious/ports/http/templates/get.tmpl @@ -0,0 +1,76 @@ +{{ define "course_info" }} +
+ +
+
+
+ +
+
+
+
+

{{ .Name }}

+

{{ .Description }}

+
+
+

{{ .FullPrice }} rub.

+
+ + + +
+
+ +
+{{ end }} + +{{ define "edit_description" }} +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+

Full price: {{ .FullPrice }}

+
+
+

+ +

+

+ +

+
+ +
+{{ end }} + diff --git a/internal/kurious/ports/http/templates/list.tmpl b/internal/kurious/ports/http/templates/list.tmpl index 54fed9e..125fd09 100644 --- a/internal/kurious/ports/http/templates/list.tmpl +++ b/internal/kurious/ports/http/templates/list.tmpl @@ -1,86 +1,69 @@ {{define "courses"}} - - - - Courses - - - -
-

My Product

- -
-

Courses

- {{range $category, $courses := .Courses}} -

{{$category}}

-

{{$category.Description}}

- {{range $course := $courses}} -
-

{{$course.Name}}

-

{{$course.Description}}

-

Full price: {{$course.FullPrice}}

-

Discount: {{$course.Discount}}

-

Thematic: {{$course.Thematic}}

-

Learning type: {{$course.LearningType}}

-

Duration: {{$course.Duration}}

-

Starts at: {{$course.StartsAt}}

-
- {{end}} - {{end}} - - + + {{ template "html_head" . }} + + {{ template "header" . }} + + + +
+
+

Welcome to the Course Aggregator

+
+ {{ range $category := .Categories }} +
{{ $category.Name }}
+

{{ $category.Description }}

+ + {{ range $subcategory := $category.Subcategories }} +
{{ $subcategory.Name }}
+

{{ $subcategory.Description }}

+ +
+ {{ range $course := $subcategory.Courses }} + {{ template "course_info" $course }} + {{ end }} +
+ {{ end }} + {{ end }} +
+
+
+ + +
+ + + + {{ template "footer" . }} + + + {{end}} diff --git a/pkg/slices/map.go b/pkg/slices/map.go index aad98e1..9eb12db 100644 --- a/pkg/slices/map.go +++ b/pkg/slices/map.go @@ -9,3 +9,9 @@ func Map[S any, E any](s []S, f func(S) E) []E { return out } + +func ForEach[S any](s []S, f func(S)) { + for i := range s { + f(s[i]) + } +}