add favicon and breadcumbs
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
./assets/kurious binary
|
||||||
6
assets/kurious/about.txt
Normal file
6
assets/kurious/about.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
This favicon was generated using the following font:
|
||||||
|
|
||||||
|
- Font Title: Lemon
|
||||||
|
- Font Author: Copyright 2011 The Lemon Project Authors (https://github.com/etunni/lemon) with Reserved Font Name "Lemon"
|
||||||
|
- Font Source: http://fonts.gstatic.com/s/lemon/v17/HI_EiYEVKqRMq0jBSZXAQ4-d.ttf
|
||||||
|
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))
|
||||||
BIN
assets/kurious/android-chrome-192x192.png
Normal file
BIN
assets/kurious/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
assets/kurious/android-chrome-512x512.png
Normal file
BIN
assets/kurious/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/kurious/apple-touch-icon.png
Normal file
BIN
assets/kurious/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed robots.txt static/*
|
//go:embed *
|
||||||
var root embed.FS
|
var root embed.FS
|
||||||
|
|
||||||
func AsHTTPFileHandler() http.Handler {
|
func AsHTTPFileHandler() http.Handler {
|
||||||
|
|||||||
BIN
assets/kurious/favicon-16x16.png
Normal file
BIN
assets/kurious/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 343 B |
BIN
assets/kurious/favicon-32x32.png
Normal file
BIN
assets/kurious/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 797 B |
BIN
assets/kurious/favicon.ico
Normal file
BIN
assets/kurious/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
assets/kurious/site.webmanifest
Normal file
1
assets/kurious/site.webmanifest
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
@ -14,11 +14,6 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
pathParamLearningType = "learning_type"
|
|
||||||
pathParamThematicType = "thematic_type"
|
|
||||||
)
|
|
||||||
|
|
||||||
func makePathTemplate(params ...string) string {
|
func makePathTemplate(params ...string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, param := range params {
|
for _, param := range params {
|
||||||
@ -41,9 +36,9 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server
|
|||||||
router.HandleFunc("/updatedesc", coursesAPI.UdpateDescription).Methods(http.MethodPost)
|
router.HandleFunc("/updatedesc", coursesAPI.UdpateDescription).Methods(http.MethodPost)
|
||||||
coursesRouter := router.PathPrefix("/courses").Subrouter()
|
coursesRouter := router.PathPrefix("/courses").Subrouter()
|
||||||
coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet)
|
coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet)
|
||||||
coursesListLearningOnlyPath := makePathTemplate(pathParamLearningType)
|
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
|
||||||
coursesRouter.HandleFunc(coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
|
coursesRouter.HandleFunc(coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
|
||||||
coursesListFullPath := makePathTemplate(pathParamLearningType, pathParamThematicType)
|
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
|
||||||
coursesRouter.HandleFunc(coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
|
coursesRouter.HandleFunc(coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
|
||||||
|
|
||||||
courseRouter := router.PathPrefix("/course").PathPrefix("/{course_id}").Subrouter()
|
courseRouter := router.PathPrefix("/course").PathPrefix("/{course_id}").Subrouter()
|
||||||
@ -56,9 +51,29 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server
|
|||||||
fs := http.FileServer(http.Dir("./assets/kurious/static/"))
|
fs := http.FileServer(http.Dir("./assets/kurious/static/"))
|
||||||
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs)).Methods(http.MethodGet)
|
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs)).Methods(http.MethodGet)
|
||||||
|
|
||||||
router.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
registerFile := func(filepath string) {
|
||||||
http.ServeFile(w, r, "./assets/kurious/robots.txt")
|
if !strings.HasPrefix(filepath, "/") {
|
||||||
}).Methods(http.MethodGet)
|
filepath = "/" + filepath
|
||||||
|
}
|
||||||
|
|
||||||
|
relativePath := "./assets/kurious" + filepath
|
||||||
|
router.HandleFunc(filepath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, relativePath)
|
||||||
|
}).Methods(http.MethodGet)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range []string{
|
||||||
|
"robots.txt",
|
||||||
|
"android-chrome-192x192.png",
|
||||||
|
"android-chrome-512x512.png",
|
||||||
|
"apple-touch-icon.png",
|
||||||
|
"favicon-16x16.png",
|
||||||
|
"favicon-32x32.png",
|
||||||
|
"favicon.ico",
|
||||||
|
"site.webmanifest",
|
||||||
|
} {
|
||||||
|
registerFile(file)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fs := kurious.AsHTTPFileHandler()
|
fs := kurious.AsHTTPFileHandler()
|
||||||
router.PathPrefix("/").Handler(fs).Methods(http.MethodGet)
|
router.PathPrefix("/").Handler(fs).Methods(http.MethodGet)
|
||||||
|
|||||||
@ -322,7 +322,7 @@ func (r *ydbCourseRepository) ListCourseThematics(
|
|||||||
|
|
||||||
learningTypeValue := types.TextValue(params.LearningTypeID)
|
learningTypeValue := types.TextValue(params.LearningTypeID)
|
||||||
d := queryTemplateDeclaration{
|
d := queryTemplateDeclaration{
|
||||||
Name: "course_thematic",
|
Name: "learning_type",
|
||||||
Type: learningTypeValue.Type().String(),
|
Type: learningTypeValue.Type().String(),
|
||||||
}
|
}
|
||||||
qtParams.Declares = append(qtParams.Declares, d)
|
qtParams.Declares = append(qtParams.Declares, d)
|
||||||
|
|||||||
@ -44,6 +44,11 @@ func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
LearningTypePathParam = "learning_type"
|
||||||
|
ThematicTypePathParam = "thematic_type"
|
||||||
|
)
|
||||||
|
|
||||||
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
||||||
out.pagination, err = parsePaginationFromQuery(r)
|
out.pagination, err = parsePaginationFromQuery(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -51,8 +56,8 @@ func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
out.learningType = vars["learning_type"]
|
out.learningType = vars[LearningTypePathParam]
|
||||||
out.courseThematic = vars["thematic_type"]
|
out.courseThematic = vars[ThematicTypePathParam]
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@ -82,9 +87,23 @@ type subcategoryInfo struct {
|
|||||||
Courses []domain.Course
|
Courses []domain.Course
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IDNamePair struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
IsActive bool
|
||||||
|
}
|
||||||
|
|
||||||
type listCoursesTemplateParams struct {
|
type listCoursesTemplateParams struct {
|
||||||
Categories []categoryInfo
|
Categories []categoryInfo
|
||||||
NextPageToken string
|
NextPageToken string
|
||||||
|
AvailableLearningTypes []IDNamePair
|
||||||
|
AvailableCourseThematics []IDNamePair
|
||||||
|
|
||||||
|
ActiveLearningType string
|
||||||
|
LearningTypeName string
|
||||||
|
|
||||||
|
ActiveCourseThematic string
|
||||||
|
CourseThematicName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapDomainCourseToTemplate(in ...domain.Course) listCoursesTemplateParams {
|
func mapDomainCourseToTemplate(in ...domain.Course) listCoursesTemplateParams {
|
||||||
@ -149,6 +168,45 @@ func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
|
|||||||
templateCourses := mapDomainCourseToTemplate(courses...)
|
templateCourses := mapDomainCourseToTemplate(courses...)
|
||||||
templateCourses.NextPageToken = result.NextPageToken
|
templateCourses.NextPageToken = result.NextPageToken
|
||||||
|
|
||||||
|
learningTypeList, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templateCourses.AvailableLearningTypes = xslices.Map(learningTypeList.LearningTypes, func(in query.LearningType) IDNamePair {
|
||||||
|
if in.ID == params.learningType {
|
||||||
|
templateCourses.LearningTypeName = in.Name
|
||||||
|
}
|
||||||
|
return IDNamePair{
|
||||||
|
ID: in.ID,
|
||||||
|
Name: in.Name,
|
||||||
|
IsActive: in.ID == params.learningType,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
templateCourses.ActiveLearningType = params.learningType
|
||||||
|
templateCourses.ActiveCourseThematic = params.courseThematic
|
||||||
|
|
||||||
|
if params.learningType != "" {
|
||||||
|
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
|
||||||
|
LearningTypeID: params.learningType,
|
||||||
|
})
|
||||||
|
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
templateCourses.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) IDNamePair {
|
||||||
|
if in.ID == params.courseThematic {
|
||||||
|
templateCourses.CourseThematicName = in.Name
|
||||||
|
}
|
||||||
|
return IDNamePair{
|
||||||
|
ID: in.ID,
|
||||||
|
Name: in.Name,
|
||||||
|
IsActive: in.ID == params.courseThematic,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
err = getCoreTemplate(ctx, c.log).ExecuteTemplate(w, "courses", templateCourses)
|
err = getCoreTemplate(ctx, c.log).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
|
||||||
|
|||||||
@ -8,6 +8,11 @@
|
|||||||
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
|
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,45 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Welcome to the Course Aggregator</h1>
|
<h1>Welcome to the Course Aggregator</h1>
|
||||||
|
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/courses">Courses</a></li>
|
||||||
|
{{ if ne .LearningTypeName "" }}
|
||||||
|
<li><a href="/courses/{{.ActiveLearningType}}">{{ .LearningTypeName }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
{{ if ne .CourseThematicName "" }}
|
||||||
|
<li><a href="/courses/{{.ActiveLearningType}}/{{.ActiveCourseThematic}}">{{ .CourseThematicName }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
<!-- <li class="is-active"><a href="#" aria-current="page">Breadcrumb</a></li> -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<form id="filter-form">
|
||||||
|
<div class="select columns">
|
||||||
|
<select id="learning-type-filter" name="learning_type">
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
>All Learning Types</option>
|
||||||
|
{{ range $t := .AvailableLearningTypes }}
|
||||||
|
<option
|
||||||
|
value="{{$t.ID}}"
|
||||||
|
{{ if eq $t.ID $.ActiveLearningType }}selected{{ end }}
|
||||||
|
>{{ $t.Name }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
<select id="course-thematic-filter" name="learning_type">
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
>All Course Thematics</option>
|
||||||
|
{{ range $t := .AvailableCourseThematics }}
|
||||||
|
<option
|
||||||
|
value="{{$t.ID}}"
|
||||||
|
{{ if eq $t.ID $.ActiveCourseThematic }}selected{{ end }}
|
||||||
|
>{{ $t.Name }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div id="category-course-list">
|
<div id="category-course-list">
|
||||||
{{ range $category := .Categories }}
|
{{ range $category := .Categories }}
|
||||||
<div class="title">{{ $category.Name }}</div>
|
<div class="title">{{ $category.Name }}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user