add pagination
This commit is contained in:
@ -1 +1 @@
|
||||
3cf236a901d03e42352790df844d58c5
|
||||
de22f926e2940e47830ef3a33736db71
|
||||
|
||||
@ -43,9 +43,9 @@ func setupCoursesHTTP(srv xhttp.Server, router *mux.Router, _ *slog.Logger) {
|
||||
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
|
||||
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
|
||||
|
||||
muxHandleFunc(coursesRouter, "/", coursesAPI.Index).Methods(http.MethodGet)
|
||||
muxHandleFunc(coursesRouter, coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
|
||||
muxHandleFunc(coursesRouter, coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
|
||||
muxHandleFunc(coursesRouter, "index", "/", coursesAPI.Index).Methods(http.MethodGet)
|
||||
muxHandleFunc(coursesRouter, "list_learning", coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
|
||||
muxHandleFunc(coursesRouter, "list_full", coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server {
|
||||
@ -111,6 +111,7 @@ func middlewareTrace() mux.MiddlewareFunc {
|
||||
reqidAttr := attribute.Key("http.request_id")
|
||||
statusAttr := attribute.Key("http.status_code")
|
||||
payloadAttr := attribute.Key("http.payload_size")
|
||||
pathAttr := attribute.Key("http.template_path")
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -118,7 +119,16 @@ func middlewareTrace() mux.MiddlewareFunc {
|
||||
reqid := xcontext.GetRequestID(ctx)
|
||||
|
||||
var span trace.Span
|
||||
ctx, span = webtracer.Start(ctx, r.URL.String(), trace.WithAttributes(reqidAttr.String(reqid)))
|
||||
route := mux.CurrentRoute(r)
|
||||
hname := route.GetName()
|
||||
hpath, _ := route.GetPathTemplate()
|
||||
ctx, span = webtracer.Start(
|
||||
ctx, "http."+hname,
|
||||
trace.WithAttributes(
|
||||
reqidAttr.String(reqid),
|
||||
pathAttr.String(hpath),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
@ -131,6 +141,8 @@ func middlewareTrace() mux.MiddlewareFunc {
|
||||
)
|
||||
if statusCode > 399 {
|
||||
span.SetStatus(codes.Error, "error during request")
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "request completed")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -22,7 +22,7 @@ import (
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
)
|
||||
|
||||
var webtracer = otel.Tracer("kuriweb")
|
||||
var webtracer = otel.Tracer("kuriweb.http")
|
||||
|
||||
type shutdownFunc func(context.Context) error
|
||||
|
||||
@ -126,7 +126,7 @@ func newMeterProvider() (*metric.MeterProvider, error) {
|
||||
return meterProvider, nil
|
||||
}
|
||||
|
||||
func muxHandleFunc(router *mux.Router, path string, hf http.HandlerFunc) *mux.Route {
|
||||
func muxHandleFunc(router *mux.Router, name, path string, hf http.HandlerFunc) *mux.Route {
|
||||
h := otelhttp.WithRouteTag(path, hf)
|
||||
return router.Handle(path, h)
|
||||
return router.Handle(path, h).Name(name)
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
<p class="justify-content-center">Here you can find course for any taste</p>
|
||||
</div>
|
||||
|
||||
<div class="container w-75">
|
||||
<div class="container w-75 mb-4">
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
@ -55,6 +55,11 @@
|
||||
<hr/>
|
||||
<p>In this category you can find courses of types such as</p>
|
||||
<p>web-development, backend development, frontend developent</p>
|
||||
<ul>
|
||||
<li><span class="d-inline-block text-truncate col-8">web-development</span></li>
|
||||
<li>backend development</li>
|
||||
<li>frontend development</li>
|
||||
</ul>
|
||||
<p>This category contains <span>128</span> courses.</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||
@ -127,56 +132,19 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row categories">
|
||||
<div class="col">
|
||||
<div class="block">
|
||||
<ul>
|
||||
<p>Category 1</p>
|
||||
<li>
|
||||
<div class="block">item</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="block">item</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="block">item</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="block">
|
||||
<ul>
|
||||
<p>Category 2</p>
|
||||
<li>
|
||||
<div class="block">item</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="block">item</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="block">item</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="block">
|
||||
<ul>
|
||||
<p>Category 3</p>
|
||||
<li>
|
||||
<div class="block">item</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="block">item</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="block">item</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link">Previous</a>
|
||||
</li>
|
||||
<li class="page-item active"><a class="page-link" href="#">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="#">2</a></li>
|
||||
<li class="page-item"><a class="page-link" href="#">3</a></li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
@ -11,9 +11,9 @@ type inMemoryMapper struct {
|
||||
courseThematicsByID map[string]string
|
||||
learningTypeByID map[string]string
|
||||
|
||||
courseThematicsCountByID map[string]int
|
||||
learningTypeCountByID map[string]int
|
||||
totalCount int
|
||||
stats map[string]domain.LearningTypeStat
|
||||
courseThematicByLearningType map[string]string
|
||||
totalCount int
|
||||
}
|
||||
|
||||
func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryMapper {
|
||||
@ -26,8 +26,8 @@ func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryM
|
||||
func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepository) error {
|
||||
const batchSize = 1000
|
||||
|
||||
m.courseThematicsCountByID = map[string]int{}
|
||||
m.learningTypeCountByID = map[string]int{}
|
||||
m.stats = map[string]domain.LearningTypeStat{}
|
||||
m.courseThematicByLearningType = map[string]string{}
|
||||
|
||||
var nextPageToken string
|
||||
for {
|
||||
@ -42,8 +42,14 @@ func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepo
|
||||
}
|
||||
m.totalCount += len(result.Courses)
|
||||
for _, course := range result.Courses {
|
||||
m.courseThematicsCountByID[course.ThematicID]++
|
||||
m.learningTypeCountByID[course.LearningTypeID]++
|
||||
stat, ok := m.stats[course.LearningTypeID]
|
||||
stat.Count++
|
||||
if !ok {
|
||||
stat.CourseThematic = map[string]int{}
|
||||
}
|
||||
stat.CourseThematic[course.ThematicID]++
|
||||
m.stats[course.LearningTypeID] = stat
|
||||
m.courseThematicByLearningType[course.ThematicID] = course.LearningTypeID
|
||||
}
|
||||
if len(result.Courses) < batchSize {
|
||||
break
|
||||
@ -55,13 +61,36 @@ func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepo
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) GetCounts(byCourseThematic, byLearningType string) int {
|
||||
if byCourseThematic != "" {
|
||||
return m.courseThematicsCountByID[byCourseThematic]
|
||||
} else if byLearningType != "" {
|
||||
return m.learningTypeCountByID[byLearningType]
|
||||
} else {
|
||||
return m.totalCount
|
||||
if byCourseThematic != "" && byLearningType == "" {
|
||||
byLearningType = m.courseThematicByLearningType[byCourseThematic]
|
||||
}
|
||||
if byLearningType != "" {
|
||||
stat := m.stats[byLearningType]
|
||||
if byCourseThematic != "" {
|
||||
return stat.CourseThematic[byCourseThematic]
|
||||
}
|
||||
return stat.Count
|
||||
}
|
||||
return m.totalCount
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) GetStats(copyMap bool) map[string]domain.LearningTypeStat {
|
||||
if !copyMap {
|
||||
return m.stats
|
||||
}
|
||||
|
||||
out := make(map[string]domain.LearningTypeStat, len(m.stats))
|
||||
for learningType, stats := range m.stats {
|
||||
copiedStats := make(map[string]int, len(stats.CourseThematic))
|
||||
for courseThematic, count := range stats.CourseThematic {
|
||||
copiedStats[courseThematic] = count
|
||||
}
|
||||
out[learningType] = domain.LearningTypeStat{
|
||||
Count: stats.Count,
|
||||
CourseThematic: copiedStats,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) CourseThematicNameByID(id string) string {
|
||||
|
||||
@ -11,14 +11,19 @@ import (
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
"git.loyso.art/frx/kurious/migrations/sqlite"
|
||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var sqliteTracer = otel.Tracer("sqlite")
|
||||
|
||||
type sqliteConnection struct {
|
||||
db *sqlx.DB
|
||||
shutdownTimeout time.Duration
|
||||
@ -68,8 +73,20 @@ func (r *sqliteCourseRepository) List(
|
||||
) (result domain.ListCoursesResult, err error) {
|
||||
const queryTemplate = `SELECT %s from courses WHERE 1=1`
|
||||
|
||||
ctx, span := sqliteTracer.Start(ctx, "sqlite.list")
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
if params.NextPageToken != "" && params.Offset > 0 {
|
||||
panic("could not use next_page_token and offset at the same time")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
|
||||
args := make([]any, 0, 1)
|
||||
args := make([]any, 0, 6)
|
||||
if params.LearningType != "" {
|
||||
args = append(args, params.LearningType)
|
||||
query += " AND learning_type = ?"
|
||||
@ -87,13 +104,20 @@ func (r *sqliteCourseRepository) List(
|
||||
query += " AND id > ?"
|
||||
}
|
||||
|
||||
query += " ORDER BY id ASC"
|
||||
query += " ORDER BY course_thematic, learning_type, id ASC"
|
||||
|
||||
if params.Limit > 0 {
|
||||
query += " LIMIT ?"
|
||||
args = append(args, params.Limit)
|
||||
}
|
||||
if params.Offset > 0 {
|
||||
query += " OFFSET ?"
|
||||
args = append(args, params.Offset)
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("query", query),
|
||||
)
|
||||
scanF := func(s rowsScanner) (err error) {
|
||||
var cdb sqliteCourseDB
|
||||
err = s.StructScan(&cdb)
|
||||
@ -114,6 +138,15 @@ func (r *sqliteCourseRepository) List(
|
||||
result.NextPageToken = result.Courses[lastIDx].ID
|
||||
}
|
||||
|
||||
result.Count, err = r.listCount(ctx, params)
|
||||
if err != nil {
|
||||
xcontext.LogWithWarnError(ctx, r.log, err, "unable to list count")
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int("items_count", len(result.Courses)),
|
||||
attribute.Int("total_items", result.Count),
|
||||
)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@ -223,6 +256,40 @@ func (r *sqliteCourseRepository) Delete(ctx context.Context, id string) error {
|
||||
return errors.New("unimplemented")
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) listCount(ctx context.Context, params domain.ListCoursesParams) (count int, err error) {
|
||||
const queryTemplate = `SELECT COUNT(id) FROM courses WHERE 1=1`
|
||||
|
||||
ctx, span := sqliteTracer.Start(ctx, "sqlite.listCount")
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
query := queryTemplate
|
||||
args := make([]any, 0, 6)
|
||||
|
||||
if params.LearningType != "" {
|
||||
args = append(args, params.LearningType)
|
||||
query += " AND learning_type = ?"
|
||||
}
|
||||
if params.CourseThematic != "" {
|
||||
args = append(args, params.CourseThematic)
|
||||
query += " AND course_thematic = ?"
|
||||
}
|
||||
if params.OrganizationID != "" {
|
||||
args = append(args, params.OrganizationID)
|
||||
query += " AND organization_id = ?"
|
||||
}
|
||||
|
||||
err = r.db.GetContext(ctx, &count, query, args...)
|
||||
if err != nil {
|
||||
return count, fmt.Errorf("sending query: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
type rowsScanner interface {
|
||||
sqlx.ColScanner
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -74,3 +75,42 @@ func (s *sqliteCourseRepositorySuite) TestCreateCourse() {
|
||||
|
||||
s.Require().Equal(expcourse, gotCourse)
|
||||
}
|
||||
|
||||
func (s *sqliteCourseRepositorySuite) TestListLimitOffset() {
|
||||
const coursecount = 9
|
||||
const listparts = 3
|
||||
basecourse := domain.CreateCourseParams{
|
||||
SourceType: domain.SourceTypeManual,
|
||||
}
|
||||
|
||||
cr := s.connection.CourseRepository()
|
||||
for i := 0; i < coursecount; i++ {
|
||||
basecourse.ID = strconv.Itoa(i)
|
||||
_, err := cr.Create(s.ctx, basecourse)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
params := domain.ListCoursesParams{
|
||||
Limit: coursecount / listparts,
|
||||
}
|
||||
for i := 0; i < listparts; i++ {
|
||||
result, err := cr.List(s.ctx, params)
|
||||
s.NoError(err)
|
||||
s.Len(result.Courses, listparts)
|
||||
|
||||
params.Offset += listparts
|
||||
}
|
||||
|
||||
result, err := cr.List(s.ctx, params)
|
||||
s.NoError(err)
|
||||
s.Empty(result.Courses)
|
||||
|
||||
params.Offset = 0
|
||||
for i := 0; i < listparts; i++ {
|
||||
result, err := cr.List(s.ctx, params)
|
||||
s.NoError(err)
|
||||
s.Len(result.Courses, listparts)
|
||||
|
||||
params.NextPageToken = result.NextPageToken
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,10 +15,11 @@ type Commands struct {
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
GetCourse query.GetCourseHandler
|
||||
ListCourses query.ListCourseHandler
|
||||
ListLearningTypes query.ListLearningTypesHandler
|
||||
ListCourseThematics query.ListCourseThematicsHandler
|
||||
GetCourse query.GetCourseHandler
|
||||
ListCourses query.ListCourseHandler
|
||||
ListLearningTypes query.ListLearningTypesHandler
|
||||
ListCourseThematics query.ListCourseThematicsHandler
|
||||
ListCourseStatistics query.ListCoursesStatsHandler
|
||||
|
||||
ListOrganzations query.ListOrganizationsHandler
|
||||
GetOrganization query.GetOrganizationHandler
|
||||
|
||||
@ -18,6 +18,7 @@ type ListCourse struct {
|
||||
Keyword string
|
||||
|
||||
Limit int
|
||||
Offset int
|
||||
NextPageToken string
|
||||
}
|
||||
|
||||
@ -41,11 +42,14 @@ func NewListCourseHandler(
|
||||
}
|
||||
|
||||
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) {
|
||||
out.AvailableCoursesOfSub = map[string]int{}
|
||||
const defaultBatchSize = 1_000
|
||||
|
||||
out.NextPageToken = query.NextPageToken
|
||||
drainFull := query.Limit == 0
|
||||
if !drainFull {
|
||||
out.Courses = make([]domain.Course, 0, query.Limit)
|
||||
} else {
|
||||
query.Limit = defaultBatchSize
|
||||
}
|
||||
for {
|
||||
result, err := h.repo.List(ctx, domain.ListCoursesParams{
|
||||
@ -53,8 +57,8 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do
|
||||
LearningType: query.LearningType,
|
||||
OrganizationID: query.OrganizationID,
|
||||
Limit: query.Limit,
|
||||
|
||||
NextPageToken: out.NextPageToken,
|
||||
Offset: query.Offset,
|
||||
NextPageToken: out.NextPageToken,
|
||||
})
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("listing courses: %w", err)
|
||||
@ -69,19 +73,15 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do
|
||||
|
||||
out.Courses = append(out.Courses, result.Courses...)
|
||||
out.NextPageToken = result.NextPageToken
|
||||
out.Count = result.Count
|
||||
|
||||
if drainFull && len(result.Courses) > 0 && result.NextPageToken != "" {
|
||||
if drainFull && len(result.Courses) == query.Limit {
|
||||
query.Offset += query.Limit
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
for _, course := range out.Courses {
|
||||
if _, ok := out.AvailableCoursesOfSub[course.ThematicID]; !ok {
|
||||
out.AvailableCoursesOfSub[course.ThematicID] = h.mapper.GetCounts(course.ThematicID, course.LearningTypeID)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
38
internal/kurious/app/query/listcoursesstats.go
Normal file
38
internal/kurious/app/query/listcoursesstats.go
Normal file
@ -0,0 +1,38 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type ListCoursesStats struct{}
|
||||
type ListCoursesStatsResult struct{}
|
||||
|
||||
type ListCoursesStatsHandler decorator.QueryHandler[ListCoursesStats, domain.ListCoursesStatsResult]
|
||||
|
||||
type listCoursesStatsHandler struct {
|
||||
mapper domain.CourseMapper
|
||||
}
|
||||
|
||||
func NewListCoursesStatsHandler(
|
||||
mapper domain.CourseMapper,
|
||||
log *slog.Logger,
|
||||
) ListCoursesStatsHandler {
|
||||
h := listCoursesStatsHandler{
|
||||
mapper: mapper,
|
||||
}
|
||||
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h listCoursesStatsHandler) Handle(
|
||||
ctx context.Context,
|
||||
query ListCoursesStats,
|
||||
) (out domain.ListCoursesStatsResult, err error) {
|
||||
stats := h.mapper.GetStats(false)
|
||||
out.StatsByLearningType = stats
|
||||
return out, nil
|
||||
}
|
||||
@ -8,4 +8,5 @@ type CourseMapper interface {
|
||||
|
||||
CollectCounts(context.Context, CourseRepository) error
|
||||
GetCounts(byCourseThematic, byLearningType string) int
|
||||
GetStats(copyMap bool) map[string]LearningTypeStat
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ type ListCoursesParams struct {
|
||||
|
||||
NextPageToken string
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type CreateCourseParams struct {
|
||||
@ -34,10 +35,19 @@ type CreateCourseParams struct {
|
||||
StartsAt time.Time
|
||||
}
|
||||
|
||||
type LearningTypeStat struct {
|
||||
Count int
|
||||
CourseThematic map[string]int
|
||||
}
|
||||
|
||||
type ListCoursesStatsResult struct {
|
||||
StatsByLearningType map[string]LearningTypeStat
|
||||
}
|
||||
|
||||
type ListCoursesResult struct {
|
||||
Courses []Course
|
||||
AvailableCoursesOfSub map[string]int
|
||||
NextPageToken string
|
||||
Courses []Course
|
||||
NextPageToken string
|
||||
Count int
|
||||
}
|
||||
|
||||
type ListLearningTypeResult struct {
|
||||
|
||||
@ -81,7 +81,7 @@ templ footer() {
|
||||
</footer>
|
||||
}
|
||||
|
||||
templ root(page PageKind, s stats) {
|
||||
templ root(page PageKind, _ stats) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
@head(string(page))
|
||||
|
||||
@ -201,7 +201,7 @@ func footer() templ.Component {
|
||||
})
|
||||
}
|
||||
|
||||
func root(page PageKind, s stats) templ.Component {
|
||||
func root(page PageKind, _ stats) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
|
||||
@ -169,5 +169,6 @@ templ ListCourses(pageType PageKind, s stats, params ListCoursesParams) {
|
||||
@listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams)
|
||||
@listCoursesSectionFilters(params.FilterForm)
|
||||
@listCoursesLearning(params.Categories)
|
||||
@pagination(params.Pagination)
|
||||
}
|
||||
}
|
||||
|
||||
@ -750,6 +750,14 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = pagination(params.Pagination).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
||||
}
|
||||
|
||||
@ -3,10 +3,11 @@ package bootstrap
|
||||
import "strconv"
|
||||
|
||||
type IndexCourseCategoryItem struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Count int
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
ExampleThemes []string
|
||||
Count int
|
||||
}
|
||||
|
||||
// courseItemCard is a card that renders a single course thematic item
|
||||
@ -18,6 +19,16 @@ templ courseItemCard(item IndexCourseCategoryItem) {
|
||||
<h5 class="card-title">{ item.Name }</h5>
|
||||
<hr/>
|
||||
<p>{ item.Description }</p>
|
||||
if len(item.ExampleThemes) > 0 {
|
||||
<p>В данной категории вы можете найти курсы по темам:</p>
|
||||
<ul>
|
||||
for _, exampleItem := range item.ExampleThemes {
|
||||
<li>
|
||||
<span class="d-inline-block text-truncate col-8">{ exampleItem }</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a
|
||||
href={ templ.URL("/courses/" + item.ID) }
|
||||
@ -34,7 +45,7 @@ templ courseItemCard(item IndexCourseCategoryItem) {
|
||||
}
|
||||
|
||||
templ courseCategory(items []IndexCourseCategoryItem) {
|
||||
<div class="container w-75">
|
||||
<div class="container w-75 mb-4">
|
||||
<div class="row g-4">
|
||||
for _, item := range items {
|
||||
<div class="col-12 col-md-8 col-lg-4">
|
||||
@ -45,14 +56,57 @@ templ courseCategory(items []IndexCourseCategoryItem) {
|
||||
</div>
|
||||
}
|
||||
|
||||
type MainPageParams struct{
|
||||
type Pagination struct {
|
||||
Page int
|
||||
TotalPages int
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
templ pagination(p Pagination) {
|
||||
if p.Page > 0 {
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li
|
||||
class={
|
||||
"page-item",
|
||||
templ.KV("disabled", p.Page > 0 && p.Page == 1),
|
||||
}
|
||||
>
|
||||
<a href={templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page - 1))} class="page-link">Previous</a>
|
||||
</li>
|
||||
for i := max(p.Page-2, 1); i < min(p.TotalPages, 10); i++ {
|
||||
<li
|
||||
class={
|
||||
"page-item",
|
||||
templ.KV("active", p.Page == i),
|
||||
}
|
||||
>
|
||||
<a href={templ.URL(p.BaseURL + "?page=" + strconv.Itoa(i))} class="page-link">{ strconv.Itoa(i) }</a>
|
||||
</li>
|
||||
}
|
||||
<li
|
||||
class={
|
||||
"page-item",
|
||||
templ.KV("disabled", p.Page == p.TotalPages),
|
||||
}
|
||||
>
|
||||
<a href={templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page + 1))} class="page-link">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
type MainPageParams struct {
|
||||
Breadcrumbs BreadcrumbsParams
|
||||
Categories []IndexCourseCategoryItem
|
||||
Categories []IndexCourseCategoryItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
templ MainPage(pageType PageKind, s stats, params MainPageParams) {
|
||||
@root(pageType, s) {
|
||||
@listCoursesSectionHeader(params.Breadcrumbs)
|
||||
@courseCategory(params.Categories)
|
||||
@pagination(params.Pagination)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,10 +13,11 @@ import "bytes"
|
||||
import "strconv"
|
||||
|
||||
type IndexCourseCategoryItem struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Count int
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
ExampleThemes []string
|
||||
Count int
|
||||
}
|
||||
|
||||
// courseItemCard is a card that renders a single course thematic item
|
||||
@ -42,7 +43,7 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 17, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 18, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -55,18 +56,60 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 19, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 20, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"d-flex justify-content-between align-items-center\"><a href=\"")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.SafeURL = templ.URL("/courses/" + item.ID)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
|
||||
if len(item.ExampleThemes) > 0 {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>")
|
||||
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("</p><ul>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, exampleItem := range item.ExampleThemes {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><span class=\"d-inline-block text-truncate col-8\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(exampleItem)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 26, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"d-flex justify-content-between align-items-center\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 templ.SafeURL = templ.URL("/courses/" + item.ID)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var6)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -74,8 +117,8 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var5 := `Open`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
templ_7745c5c3_Var7 := `Open`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -83,12 +126,12 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(item.Count))
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(item.Count))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 28, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 39, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -96,8 +139,8 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var7 := `items.`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
templ_7745c5c3_Var9 := `items.`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -120,12 +163,12 @@ func courseCategory(items []IndexCourseCategoryItem) templ.Component {
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var8 == nil {
|
||||
templ_7745c5c3_Var8 = templ.NopComponent
|
||||
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var10 == nil {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container w-75\"><div class=\"row g-4\">")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container w-75 mb-4\"><div class=\"row g-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -154,9 +197,162 @@ func courseCategory(items []IndexCourseCategoryItem) templ.Component {
|
||||
})
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int
|
||||
TotalPages int
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func pagination(p Pagination) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var11 == nil {
|
||||
templ_7745c5c3_Var11 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if p.Page > 0 {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav aria-label=\"Page navigation\"><ul class=\"pagination justify-content-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 = []any{
|
||||
"page-item",
|
||||
templ.KV("disabled", p.Page > 0 && p.Page == 1),
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var12).String()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page-1))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var13)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"page-link\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var14 := `Previous`
|
||||
_, 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("</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i := max(p.Page-2, 1); i < min(p.TotalPages, 10); i++ {
|
||||
var templ_7745c5c3_Var15 = []any{
|
||||
"page-item",
|
||||
templ.KV("active", p.Page == i),
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var15).String()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(i))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var16)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"page-link\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 83, Col: 101}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
var templ_7745c5c3_Var18 = []any{
|
||||
"page-item",
|
||||
templ.KV("disabled", p.Page == p.TotalPages),
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var18).String()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page+1))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var19)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"page-link\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var20 := `Next`
|
||||
_, 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("</a></li></ul></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
type MainPageParams struct {
|
||||
Breadcrumbs BreadcrumbsParams
|
||||
Categories []IndexCourseCategoryItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component {
|
||||
@ -167,12 +363,12 @@ func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var9 == nil {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
templ_7745c5c3_Var21 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var21 == nil {
|
||||
templ_7745c5c3_Var21 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var10 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Var22 := 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()
|
||||
@ -190,12 +386,20 @@ func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = pagination(params.Pagination).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
|
||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var22), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -96,6 +96,8 @@ type SubcategoryContainer struct {
|
||||
type ListCoursesParams struct {
|
||||
FilterForm FilterFormParams
|
||||
Categories []CategoryContainer
|
||||
Pagination Pagination
|
||||
Items int
|
||||
}
|
||||
|
||||
func GetOrFallback[T comparable](value T, fallback T) T {
|
||||
@ -105,3 +107,25 @@ func GetOrFallback[T comparable](value T, fallback T) T {
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
type ordered interface {
|
||||
~int8 | ~int16 | ~int32 | ~int64 | ~int |
|
||||
~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uint |
|
||||
~float32 | float64 | ~string
|
||||
}
|
||||
|
||||
func min[T ordered](lhs, rhs T) T {
|
||||
if lhs < rhs {
|
||||
return lhs
|
||||
}
|
||||
|
||||
return rhs
|
||||
}
|
||||
|
||||
func max[T ordered](lhs, rhs T) T {
|
||||
if lhs > rhs {
|
||||
return lhs
|
||||
}
|
||||
|
||||
return rhs
|
||||
}
|
||||
|
||||
@ -2,11 +2,9 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
@ -16,6 +14,7 @@ import (
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
@ -30,7 +29,7 @@ type courseTemplServer struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) bootstrap.ListCoursesParams {
|
||||
func makeTemplListCoursesParams(counts map[string]domain.LearningTypeStat, in ...domain.Course) bootstrap.ListCoursesParams {
|
||||
coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in))
|
||||
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
||||
categoryByID := make(map[string]bootstrap.CategoryBaseInfo, len(in))
|
||||
@ -61,7 +60,7 @@ func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) boot
|
||||
categoryByID[c.ThematicID] = bootstrap.CategoryBaseInfo{
|
||||
ID: c.ThematicID,
|
||||
Name: c.Thematic,
|
||||
Count: counts[c.ThematicID],
|
||||
Count: counts[c.LearningTypeID].CourseThematic[c.ThematicID],
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -91,7 +90,7 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var span trace.Span
|
||||
ctx, span = webtracer.Start(ctx, "list")
|
||||
ctx, span = webtracer.Start(ctx, "http_server.list")
|
||||
defer func() {
|
||||
span.End()
|
||||
}()
|
||||
@ -106,17 +105,26 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
jsonParams, _ := json.Marshal(pathParams)
|
||||
span.SetAttributes(paramsAttr.String(string(jsonParams)))
|
||||
|
||||
var offset int
|
||||
if pathParams.Page > 0 {
|
||||
offset = (pathParams.Page - 1) * pathParams.PerPage
|
||||
}
|
||||
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||
CourseThematic: pathParams.CourseThematic,
|
||||
LearningType: pathParams.LearningType,
|
||||
Limit: pathParams.PerPage,
|
||||
NextPageToken: pathParams.NextPageToken,
|
||||
Offset: offset,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
params := makeTemplListCoursesParams(listCoursesResult.AvailableCoursesOfSub, listCoursesResult.Courses...)
|
||||
statsresult, err := c.app.Queries.ListCourseStatistics.Handle(ctx, query.ListCoursesStats{})
|
||||
if handleError(ctx, err, w, c.log, "unable to load stats") {
|
||||
return
|
||||
}
|
||||
params := makeTemplListCoursesParams(statsresult.StatsByLearningType, listCoursesResult.Courses...)
|
||||
|
||||
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||
@ -156,12 +164,6 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
c.log.DebugContext(
|
||||
ctx, "using bootstrap",
|
||||
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
|
||||
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
|
||||
)
|
||||
|
||||
params = bootstrap.ListCoursesParams{
|
||||
FilterForm: bootstrap.FilterFormParams{
|
||||
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
|
||||
@ -172,8 +174,22 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
AvailableCourseThematics: params.FilterForm.AvailableCourseThematics,
|
||||
},
|
||||
Categories: params.Categories,
|
||||
Pagination: bootstrap.Pagination{
|
||||
Page: pathParams.Page,
|
||||
TotalPages: listCoursesResult.Count / pathParams.PerPage,
|
||||
BaseURL: r.URL.Path,
|
||||
},
|
||||
}
|
||||
|
||||
c.log.DebugContext(
|
||||
ctx, "params rendered",
|
||||
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
|
||||
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
|
||||
slog.Int("items", len(listCoursesResult.Courses)),
|
||||
slog.Int("page", params.Pagination.Page),
|
||||
slog.Int("total_pages", params.Pagination.TotalPages),
|
||||
)
|
||||
|
||||
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.CategoryContainer) int {
|
||||
if lhs.Count > rhs.Count {
|
||||
return 1
|
||||
@ -191,13 +207,15 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
if handleError(ctx, err, w, c.log, "unable to render list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
span.SetStatus(codes.Ok, "request completed")
|
||||
}
|
||||
|
||||
func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var span trace.Span
|
||||
ctx, span = webtracer.Start(ctx, "index")
|
||||
ctx, span = webtracer.Start(ctx, "http_server.index")
|
||||
defer func() {
|
||||
span.End()
|
||||
}()
|
||||
@ -238,13 +256,7 @@ func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
||||
return in.Name
|
||||
})
|
||||
|
||||
namesStr := strings.Join(names, ",")
|
||||
|
||||
category.Description = fmt.Sprintf(
|
||||
"Here you can find courses"+
|
||||
" such as %s",
|
||||
namesStr,
|
||||
)
|
||||
category.ExampleThemes = names
|
||||
|
||||
params.Categories = append(params.Categories, category)
|
||||
}
|
||||
@ -265,4 +277,5 @@ func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
||||
if handleError(ctx, err, w, c.log, "rendeting template") {
|
||||
return
|
||||
}
|
||||
span.SetStatus(codes.Ok, "request completed")
|
||||
}
|
||||
|
||||
@ -10,10 +10,10 @@ import (
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
@ -38,8 +38,8 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
|
||||
}
|
||||
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetStatus(codes.Error, "error during handling request")
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, "error during handling request")
|
||||
|
||||
var errorString string
|
||||
var code int
|
||||
@ -66,10 +66,16 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
|
||||
type pagination struct {
|
||||
NextPageToken string
|
||||
PerPage int
|
||||
Page int
|
||||
}
|
||||
|
||||
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
||||
query := r.URL.Query()
|
||||
|
||||
if query.Has("next") && query.Has("page") {
|
||||
return out, errors.NewValidationError("next", `could not be set together with "page"`)
|
||||
}
|
||||
|
||||
out.NextPageToken = query.Get("next")
|
||||
|
||||
if query.Has("per_page") {
|
||||
@ -80,6 +86,14 @@ func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
||||
} else {
|
||||
out.PerPage = 50
|
||||
}
|
||||
if query.Has("page") {
|
||||
out.Page, err = strconv.Atoi(query.Get("page"))
|
||||
if err != nil {
|
||||
return out, errors.NewValidationError("page", "bad per_page value")
|
||||
}
|
||||
} else if !query.Has("next") {
|
||||
out.Page = 1
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@ -82,10 +82,11 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.Co
|
||||
InsertOrganization: command.NewCreateOrganizationHandler(organizationrepo, log),
|
||||
},
|
||||
Queries: app.Queries{
|
||||
ListCourses: query.NewListCourseHandler(courseadapter, mapper, log),
|
||||
ListLearningTypes: query.NewListLearningTypesHandler(courseadapter, mapper, log),
|
||||
ListCourseThematics: query.NewListCourseThematicsHandler(courseadapter, mapper, log),
|
||||
GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log),
|
||||
ListCourses: query.NewListCourseHandler(courseadapter, mapper, log),
|
||||
ListLearningTypes: query.NewListLearningTypesHandler(courseadapter, mapper, log),
|
||||
ListCourseThematics: query.NewListCourseThematicsHandler(courseadapter, mapper, log),
|
||||
ListCourseStatistics: query.NewListCoursesStatsHandler(mapper, log),
|
||||
GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log),
|
||||
|
||||
ListOrganzations: query.NewListOrganizationsHandler(organizationrepo, log),
|
||||
GetOrganization: query.NewGetOrganizationHandler(organizationrepo, log),
|
||||
|
||||
Reference in New Issue
Block a user