add simple http

This commit is contained in:
Gitea
2023-12-17 10:01:49 +03:00
parent b3e14f8c45
commit f60ebcfb36
9 changed files with 393 additions and 36 deletions

View 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)
}

View File

@ -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)
}

View 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}}