add simple http
This commit is contained in:
118
internal/kurious/ports/http/course.go
Normal file
118
internal/kurious/ports/http/course.go
Normal file
@ -0,0 +1,118 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type courseServer struct {
|
||||
app app.Application
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
type pagination struct {
|
||||
page 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) {
|
||||
query := r.URL.Query()
|
||||
|
||||
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") {
|
||||
out.perPage, err = strconv.Atoi(query.Get("per_page"))
|
||||
if err != nil {
|
||||
return out, errors.NewValidationError("per_page", "bad per_page value")
|
||||
}
|
||||
} else {
|
||||
out.perPage = 50
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
||||
out.pagination, err = parsePaginationFromQuery(r)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
out.learningType = query.Get("category")
|
||||
out.courseThematic = query.Get("type")
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type listCoursesParams struct {
|
||||
pagination
|
||||
|
||||
courseThematic string
|
||||
learningType string
|
||||
}
|
||||
|
||||
type templateCourse domain.Course
|
||||
|
||||
func mapDomainCourseToTemplate(in ...domain.Course) []templateCourse {
|
||||
return xslice.Map(in, func(v domain.Course) templateCourse {
|
||||
return templateCourse(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
params, err := parseListCoursesParams(r)
|
||||
if err != nil {
|
||||
handleError(ctx, err, w, c.log, "unable to parse list courses params")
|
||||
return
|
||||
}
|
||||
|
||||
limit, offset := params.LimitOffset()
|
||||
|
||||
courses, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||
CourseThematic: params.courseThematic,
|
||||
LearningType: params.learningType,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
handleError(ctx, err, w, c.log, "unable to list courses")
|
||||
}
|
||||
|
||||
templateCourses := mapDomainCourseToTemplate(courses...)
|
||||
|
||||
err = template.Must(template.ParseFiles("templates/list.tmpl")).Execute(w, templateCourses)
|
||||
if err != nil {
|
||||
handleError(ctx, err, w, c.log, "unable to execute template")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
@ -1,3 +1,55 @@
|
||||
package http
|
||||
|
||||
type Server struct{}
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPerPage = 50
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
app app.Application
|
||||
}
|
||||
|
||||
func NewServer(app app.Application) Server {
|
||||
return Server{}
|
||||
}
|
||||
|
||||
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) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var errorString string
|
||||
var code int
|
||||
valErr := new(errors.ValidationError)
|
||||
switch {
|
||||
case stderrors.As(err, &valErr):
|
||||
errorString = valErr.Error()
|
||||
code = http.StatusBadRequest
|
||||
case stderrors.Is(err, errors.ErrNotFound):
|
||||
errorString = err.Error()
|
||||
code = http.StatusNotFound
|
||||
default:
|
||||
errorString = "internal server error"
|
||||
code = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
xcontext.LogWithWarnError(ctx, log, err, msg, slog.Int("status_code", code), slog.String("response", errorString))
|
||||
|
||||
http.Error(w, errorString, code)
|
||||
}
|
||||
|
||||
86
internal/kurious/ports/http/templates/list.tmpl
Normal file
86
internal/kurious/ports/http/templates/list.tmpl
Normal file
@ -0,0 +1,86 @@
|
||||
{{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;
|
||||
}
|
||||
.course-plate {
|
||||
background-color: #f2f2f2;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.course-plate a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
.course-plate a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>My Product</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>
|
||||
<h1>Courses</h1>
|
||||
{{range $category, $courses := .Courses}}
|
||||
<h2>{{$category}}</h2>
|
||||
<p>{{$category.Description}}</p>
|
||||
{{range $course := $courses}}
|
||||
<div class="course-plate">
|
||||
<h3><a href="/courses/{{$course.ID}}">{{$course.Name}}</a></h3>
|
||||
<p>{{$course.Description}}</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>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user