use tailwind
This commit is contained in:
@ -41,7 +41,7 @@ func setupHTTPWithTempl(srv xhttp.Server, router *mux.Router, log *slog.Logger)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupHTTPWithGoTemplates(srv xhttp.Server, router *mux.Router, log *slog.Logger) {
|
func setupHTTPWithGoTemplates(srv xhttp.Server, router *mux.Router, log *slog.Logger) {
|
||||||
coursesAPI := srv.Courses()
|
coursesAPI := srv.Courses(true)
|
||||||
|
|
||||||
coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true)
|
coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true)
|
||||||
coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet)
|
coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"html/template"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
@ -21,6 +22,8 @@ import (
|
|||||||
type courseServer struct {
|
type courseServer struct {
|
||||||
app service.Application
|
app service.Application
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
|
|
||||||
|
useTailwind bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type pagination struct {
|
type pagination struct {
|
||||||
@ -218,7 +221,13 @@ func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "courses", templateCourses)
|
var tmpl *template.Template
|
||||||
|
if c.useTailwind {
|
||||||
|
tmpl = getTemplateHTMLBySpecificFiles(ctx, c.log, "list.html")
|
||||||
|
} else {
|
||||||
|
tmpl = getCoreTemplate(ctx, c.log)
|
||||||
|
}
|
||||||
|
err = tmpl.ExecuteTemplate(w, "courses", templateCourses)
|
||||||
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
if handleError(ctx, err, w, c.log, "unable to execute template") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
20
internal/kurious/ports/http/html/index.html
Normal file
20
internal/kurious/ports/http/html/index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{{ define "base" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{ template "htmlhead" . }}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{ template "htmlbody" . }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "htmlhead" }}
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ .AppName }}</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
{{ end }}
|
||||||
14
internal/kurious/ports/http/html/list.html
Normal file
14
internal/kurious/ports/http/html/list.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{{ define "htmlbody" }}
|
||||||
|
{{ template "header" .}}
|
||||||
|
{{ template "body" .}}
|
||||||
|
{{ template "footer" .}}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "header" }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "body" }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "footer" }}
|
||||||
|
{{ end }}
|
||||||
@ -12,7 +12,11 @@ import (
|
|||||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const baseTemplatePath = "./internal/kurious/ports/http/templates/"
|
const (
|
||||||
|
baseTemplatePath = "./internal/kurious/ports/http"
|
||||||
|
templateDir = "/templates"
|
||||||
|
htmlPath = "/html"
|
||||||
|
)
|
||||||
|
|
||||||
func must[T any](t T, err error) T {
|
func must[T any](t T, err error) T {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -21,9 +25,10 @@ func must[T any](t T, err error) T {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanFiles() []string {
|
func scanFiles(dir string) []string {
|
||||||
|
dst := path.Join(baseTemplatePath, dir)
|
||||||
entries := xslices.Map(
|
entries := xslices.Map(
|
||||||
must(os.ReadDir(baseTemplatePath)),
|
must(os.ReadDir(dst)),
|
||||||
func(v fs.DirEntry) string {
|
func(v fs.DirEntry) string {
|
||||||
return path.Join(baseTemplatePath, v.Name())
|
return path.Join(baseTemplatePath, v.Name())
|
||||||
},
|
},
|
||||||
@ -32,161 +37,31 @@ func scanFiles() []string {
|
|||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTemplateHTMLBySpecificFiles(ctx context.Context, log *slog.Logger, filenames ...string) *template.Template {
|
||||||
|
filenames = append([]string{"index.html"}, filenames...)
|
||||||
|
dir := path.Join(baseTemplatePath, htmlPath)
|
||||||
|
out := xslices.Map(filenames, func(in string) string {
|
||||||
|
return path.Join(dir, in)
|
||||||
|
})
|
||||||
|
|
||||||
|
tmpl, err := template.New("courses").ParseFiles(out...)
|
||||||
|
if err != nil {
|
||||||
|
xcontext.LogWithWarnError(ctx, log, err, "unable to parse template")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpl
|
||||||
|
}
|
||||||
|
|
||||||
func getCoreTemplate(ctx context.Context, log *slog.Logger) *template.Template {
|
func getCoreTemplate(ctx context.Context, log *slog.Logger) *template.Template {
|
||||||
filenames := scanFiles()
|
filenames := scanFiles(templateDir)
|
||||||
out, err := template.New("courses").ParseFiles(filenames...)
|
out, err := template.New("courses").ParseFiles(filenames...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xcontext.LogWithWarnError(ctx, log, err, "unable to parse template")
|
xcontext.LogWithWarnError(ctx, log, err, "unable to parse template")
|
||||||
|
|
||||||
return listTemplateParsed
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
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}}`
|
|
||||||
|
|||||||
@ -23,8 +23,12 @@ func NewServer(app service.Application, log *slog.Logger) Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Server) Courses() courseServer {
|
func (s Server) Courses(useTailwind bool) courseServer {
|
||||||
return courseServer(s)
|
return courseServer{
|
||||||
|
app: s.app,
|
||||||
|
log: s.log,
|
||||||
|
useTailwind: useTailwind,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Server) CoursesByTempl() courseTemplServer {
|
func (s Server) CoursesByTempl() courseTemplServer {
|
||||||
|
|||||||
Reference in New Issue
Block a user