Beef up index or something
This commit is contained in:
parent
c9aa3149ef
commit
c550f2cd22
|
@ -7369,6 +7369,11 @@ article code {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1; } }
|
flex-shrink: 1; } }
|
||||||
|
|
||||||
|
.c--inherit {
|
||||||
|
color: inherit; }
|
||||||
|
.c--inherit:hover, .c--inherit:active {
|
||||||
|
color: inherit; }
|
||||||
|
|
||||||
.b--theme {
|
.b--theme {
|
||||||
border-color: #666;
|
border-color: #666;
|
||||||
border-color: var(--theme-color); }
|
border-color: var(--theme-color); }
|
||||||
|
|
|
@ -705,47 +705,8 @@ func (c *UrlContext) BuildBlogPostReply(threadId int, postId int) string {
|
||||||
* Library
|
* Library
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Any library route. Remove after we port the library.
|
|
||||||
var RegexLibraryAny = regexp.MustCompile(`^/library`)
|
var RegexLibraryAny = regexp.MustCompile(`^/library`)
|
||||||
|
|
||||||
var RegexLibrary = regexp.MustCompile(`^/library$`)
|
|
||||||
|
|
||||||
func BuildLibrary() string {
|
|
||||||
defer CatchPanic()
|
|
||||||
return Url("/library", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var RegexLibraryAll = regexp.MustCompile(`^/library/all$`)
|
|
||||||
|
|
||||||
func BuildLibraryAll() string {
|
|
||||||
defer CatchPanic()
|
|
||||||
return Url("/library/all", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var RegexLibraryTopic = regexp.MustCompile(`^/library/topic/(?P<topicid>\d+)$`)
|
|
||||||
|
|
||||||
func BuildLibraryTopic(topicId int) string {
|
|
||||||
defer CatchPanic()
|
|
||||||
if topicId < 1 {
|
|
||||||
panic(oops.New(nil, "Invalid library topic ID (%d), must be >= 1", topicId))
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder strings.Builder
|
|
||||||
builder.WriteString("/library/topic/")
|
|
||||||
builder.WriteString(strconv.Itoa(topicId))
|
|
||||||
|
|
||||||
return Url(builder.String(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var RegexLibraryResource = regexp.MustCompile(`^/library/resource/(?P<resourceid>\d+)$`)
|
|
||||||
|
|
||||||
func BuildLibraryResource(resourceId int) string {
|
|
||||||
defer CatchPanic()
|
|
||||||
builder := buildLibraryResourcePath(resourceId)
|
|
||||||
|
|
||||||
return Url(builder.String(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Episode Guide
|
* Episode Guide
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -197,6 +197,14 @@ article code {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c--inherit {
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.b--theme {
|
.b--theme {
|
||||||
@include usevar(border-color, theme-color);
|
@include usevar(border-color, theme-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,6 +213,9 @@ func UserToTemplate(u *models.User, currentTheme string) User {
|
||||||
|
|
||||||
DiscordSaveShowcase: u.DiscordSaveShowcase,
|
DiscordSaveShowcase: u.DiscordSaveShowcase,
|
||||||
DiscordDeleteSnippetOnMessageDelete: u.DiscordDeleteSnippetOnMessageDelete,
|
DiscordDeleteSnippetOnMessageDelete: u.DiscordDeleteSnippetOnMessageDelete,
|
||||||
|
|
||||||
|
IsEduTester: u.CanSeeUnpublishedEducationContent(),
|
||||||
|
IsEduAuthor: u.CanAuthorEducation(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,21 +82,28 @@
|
||||||
|
|
||||||
{{ if .ShowEduOptions }}
|
{{ if .ShowEduOptions }}
|
||||||
{{/* Hope you have a .Article field! */}}
|
{{/* Hope you have a .Article field! */}}
|
||||||
<div class="mb2">
|
<div class="bg--dim br3 pa3 mt3">
|
||||||
<label for="slug">Slug:</label>
|
<h4>Education Options</h4>
|
||||||
<input name="slug" maxlength="255" type="text" id="slug" required value="{{ .Article.Slug }}" />
|
<div class="mb2">
|
||||||
</div>
|
<label for="slug">Slug:</label>
|
||||||
<div class="mb2">
|
<input name="slug" maxlength="255" type="text" id="slug" required value="{{ .Article.Slug }}" />
|
||||||
<label for="type">Type:</label>
|
</div>
|
||||||
<select name="type" id="type">
|
<div class="mb2">
|
||||||
<option value="article" {{ if eq .Article.Type "article" }}selected{{ end }}>Article</option>
|
<label for="type">Type:</label>
|
||||||
<option value="glossary" {{ if eq .Article.Type "glossary" }}selected{{ end }}>Glossary Term</option>
|
<select name="type" id="type">
|
||||||
</select>
|
<option value="article" {{ if eq .Article.Type "article" }}selected{{ end }}>Article</option>
|
||||||
</div>
|
<option value="glossary" {{ if eq .Article.Type "glossary" }}selected{{ end }}>Glossary Term</option>
|
||||||
<div class="mb2">
|
</select>
|
||||||
<label for="description">Description:</label>
|
</div>
|
||||||
<div>
|
<div class="mb2">
|
||||||
<textarea name="description" id="slug" required>{{ .Article.Description }}</textarea>
|
<label for="description">Description:</label>
|
||||||
|
<div>
|
||||||
|
<textarea name="description" id="slug" required>{{ .Article.Description }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb2">
|
||||||
|
<label for="published">Published:</label>
|
||||||
|
<input name="published" id="published" type="checkbox" {{ if .Article.Published }}checked{{ end }}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
{{ template "base.html" . }}
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
|
{{ if .User.IsEduAuthor }}
|
||||||
|
<div class="optionbar">
|
||||||
|
<div class="options">
|
||||||
|
</div>
|
||||||
|
<div class="options">
|
||||||
|
<a class="edit action button" href="{{ .EditUrl }}" title="Edit">✎ Edit</a>
|
||||||
|
<a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">✖ Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
{{ .Article.Content }}
|
{{ .Article.Content }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,5 +1,43 @@
|
||||||
{{ template "base.html" . }}
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
O NO
|
<h1>Learn the Handmade way.</h1>
|
||||||
|
|
||||||
|
<h2>Guides</h2>
|
||||||
|
|
||||||
|
{{ if .User.IsEduAuthor }}
|
||||||
|
<div class="mb2">
|
||||||
|
<a href="{{ .NewArticleUrl }}"><span class="big pr1">+</span> New Article</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="flex flex-column g3 mb3">
|
||||||
|
{{ range .Articles }}
|
||||||
|
<a class="c--inherit flex flex-column pa3 bg--dim br2" href="{{ .Url }}" >
|
||||||
|
<h3 class="mb1 link">{{ .Title }}</h3>
|
||||||
|
<div>{{ .Description }}</div>
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>What makes us different?</h2>
|
||||||
|
|
||||||
|
<div class="flex flex-column flex-row-ns g3">
|
||||||
|
<div class="flex-fair bg--dim pa3 br2">
|
||||||
|
<h3>Real material.</h3>
|
||||||
|
|
||||||
|
We equip you to go straight to the source. Our guides are structured around books and articles written by experts. We give you high-quality material to read, and the context to understand it. You do the rest.
|
||||||
|
</div>
|
||||||
|
<div class="flex-fair bg--dim pa3 br3">
|
||||||
|
<h3>For any skill level.</h3>
|
||||||
|
|
||||||
|
Each guide runs the gamut from beginner to advanced. Whether you're new to a topic or have been practicing it for years, read through our guides and you'll find something new.
|
||||||
|
</div>
|
||||||
|
<div class="flex-fair bg--dim pa3 br3">
|
||||||
|
<h3>Designed for programmers.</h3>
|
||||||
|
|
||||||
|
We're not here to teach you how to program. We're here to teach you a specific topic.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
<div class="root-item">
|
<div class="root-item">
|
||||||
<a>Resources <div class="dib svgicon ml1">{{ svg "chevron-down-thick" }}</div></a>
|
<a>Resources <div class="dib svgicon ml1">{{ svg "chevron-down-thick" }}</div></a>
|
||||||
<div class="submenu b--theme-dark">
|
<div class="submenu b--theme-dark">
|
||||||
<a href="{{ .Header.LibraryUrl }}">Library</a>
|
<a href="{{ .Header.EducationUrl }}">Education</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -51,8 +51,8 @@ type Header struct {
|
||||||
PodcastUrl string
|
PodcastUrl string
|
||||||
FishbowlUrl string
|
FishbowlUrl string
|
||||||
ForumsUrl string
|
ForumsUrl string
|
||||||
LibraryUrl string
|
|
||||||
ConferencesUrl string
|
ConferencesUrl string
|
||||||
|
EducationUrl string
|
||||||
|
|
||||||
Project *ProjectHeader
|
Project *ProjectHeader
|
||||||
}
|
}
|
||||||
|
@ -191,6 +191,9 @@ type User struct {
|
||||||
|
|
||||||
DiscordSaveShowcase bool
|
DiscordSaveShowcase bool
|
||||||
DiscordDeleteSnippetOnMessageDelete bool
|
DiscordDeleteSnippetOnMessageDelete bool
|
||||||
|
|
||||||
|
IsEduTester bool
|
||||||
|
IsEduAuthor bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
|
|
|
@ -74,8 +74,8 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
|
||||||
PodcastUrl: hmnurl.BuildPodcast(),
|
PodcastUrl: hmnurl.BuildPodcast(),
|
||||||
FishbowlUrl: hmnurl.BuildFishbowlIndex(),
|
FishbowlUrl: hmnurl.BuildFishbowlIndex(),
|
||||||
ForumsUrl: hmnurl.HMNProjectContext.BuildForum(nil, 1),
|
ForumsUrl: hmnurl.HMNProjectContext.BuildForum(nil, 1),
|
||||||
LibraryUrl: hmnurl.BuildLibrary(),
|
|
||||||
ConferencesUrl: hmnurl.BuildConferences(),
|
ConferencesUrl: hmnurl.BuildConferences(),
|
||||||
|
EducationUrl: hmnurl.BuildEducationIndex(),
|
||||||
},
|
},
|
||||||
Footer: templates.Footer{
|
Footer: templates.Footer{
|
||||||
HomepageUrl: hmnurl.BuildHomepage(),
|
HomepageUrl: hmnurl.BuildHomepage(),
|
||||||
|
|
|
@ -20,10 +20,24 @@ import (
|
||||||
func EducationIndex(c *RequestContext) ResponseData {
|
func EducationIndex(c *RequestContext) ResponseData {
|
||||||
type indexData struct {
|
type indexData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
|
Articles []templates.EduArticle
|
||||||
|
NewArticleUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
articles, err := fetchEduArticles(c, c.Conn, models.EduArticleTypeArticle, c.CurrentUser)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmplArticles []templates.EduArticle
|
||||||
|
for _, article := range articles {
|
||||||
|
tmplArticles = append(tmplArticles, templates.EducationArticleToTemplate(&article))
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := indexData{
|
tmpl := indexData{
|
||||||
BaseData: getBaseData(c, "Handmade Education", nil),
|
BaseData: getBaseData(c, "Handmade Education", nil),
|
||||||
|
Articles: tmplArticles,
|
||||||
|
NewArticleUrl: hmnurl.BuildEducationArticleNew(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
|
@ -48,10 +62,12 @@ func EducationGlossary(c *RequestContext) ResponseData {
|
||||||
func EducationArticle(c *RequestContext) ResponseData {
|
func EducationArticle(c *RequestContext) ResponseData {
|
||||||
type articleData struct {
|
type articleData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
Article templates.EduArticle
|
Article templates.EduArticle
|
||||||
|
EditUrl string
|
||||||
|
DeleteUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
article, err := fetchEduArticle(c, c.Conn, c.PathParams["slug"], models.EduArticleTypeArticle)
|
article, err := fetchEduArticle(c, c.Conn, c.PathParams["slug"], models.EduArticleTypeArticle, c.CurrentUser)
|
||||||
if errors.Is(err, db.NotFound) {
|
if errors.Is(err, db.NotFound) {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -59,9 +75,14 @@ func EducationArticle(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := articleData{
|
tmpl := articleData{
|
||||||
BaseData: getBaseData(c, "Handmade Education", nil),
|
BaseData: getBaseData(c, article.Title, nil),
|
||||||
Article: templates.EducationArticleToTemplate(article),
|
Article: templates.EducationArticleToTemplate(article),
|
||||||
|
EditUrl: hmnurl.BuildEducationArticleEdit(article.Slug),
|
||||||
|
DeleteUrl: hmnurl.BuildEducationArticleDelete(article.Slug),
|
||||||
}
|
}
|
||||||
|
tmpl.OpenGraphItems = append(tmpl.OpenGraphItems,
|
||||||
|
templates.OpenGraphItem{Property: "og:description", Value: string(article.Description)},
|
||||||
|
)
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.MustWriteTemplate("education_article.html", tmpl, c.Perf)
|
res.MustWriteTemplate("education_article.html", tmpl, c.Perf)
|
||||||
|
@ -151,7 +172,7 @@ func EducationArticleEdit(c *RequestContext) ResponseData {
|
||||||
Article templates.EduArticle
|
Article templates.EduArticle
|
||||||
}
|
}
|
||||||
|
|
||||||
article, err := fetchEduArticle(c, c.Conn, c.PathParams["slug"], 0)
|
article, err := fetchEduArticle(c, c.Conn, c.PathParams["slug"], 0, c.CurrentUser)
|
||||||
if errors.Is(err, db.NotFound) {
|
if errors.Is(err, db.NotFound) {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -175,7 +196,7 @@ func EducationArticleEditSubmit(c *RequestContext) ResponseData {
|
||||||
return c.ErrorResponse(http.StatusBadRequest, err)
|
return c.ErrorResponse(http.StatusBadRequest, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = fetchEduArticle(c, c.Conn, c.PathParams["slug"], 0)
|
_, err = fetchEduArticle(c, c.Conn, c.PathParams["slug"], 0, c.CurrentUser)
|
||||||
if errors.Is(err, db.NotFound) {
|
if errors.Is(err, db.NotFound) {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -191,7 +212,7 @@ func EducationArticleEditSubmit(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
func EducationArticleDelete(c *RequestContext) ResponseData {
|
func EducationArticleDelete(c *RequestContext) ResponseData {
|
||||||
article, err := fetchEduArticle(c, c.Conn, c.PathParams["slug"], 0)
|
article, err := fetchEduArticle(c, c.Conn, c.PathParams["slug"], 0, c.CurrentUser)
|
||||||
if errors.Is(err, db.NotFound) {
|
if errors.Is(err, db.NotFound) {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -226,7 +247,55 @@ func EducationArticleDeleteSubmit(c *RequestContext) ResponseData {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchEduArticle(ctx context.Context, dbConn db.ConnOrTx, slug string, t models.EduArticleType) (*models.EduArticle, error) {
|
func fetchEduArticles(
|
||||||
|
ctx context.Context,
|
||||||
|
dbConn db.ConnOrTx,
|
||||||
|
t models.EduArticleType,
|
||||||
|
currentUser *models.User,
|
||||||
|
) ([]models.EduArticle, error) {
|
||||||
|
type eduArticleResult struct {
|
||||||
|
Article models.EduArticle `db:"a"`
|
||||||
|
CurrentVersion models.EduArticleVersion `db:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var qb db.QueryBuilder
|
||||||
|
qb.Add(`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
education_article AS a
|
||||||
|
JOIN education_article_version AS v ON a.current_version = v.id
|
||||||
|
WHERE
|
||||||
|
TRUE
|
||||||
|
`)
|
||||||
|
if t != 0 {
|
||||||
|
qb.Add(`AND a.type = $?`, t)
|
||||||
|
}
|
||||||
|
if currentUser == nil || !currentUser.CanSeeUnpublishedEducationContent() {
|
||||||
|
qb.Add(`AND NOT a.published`)
|
||||||
|
}
|
||||||
|
|
||||||
|
articles, err := db.Query[eduArticleResult](ctx, dbConn, qb.String(), qb.Args()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res []models.EduArticle
|
||||||
|
for _, article := range articles {
|
||||||
|
ver := article.CurrentVersion
|
||||||
|
article.Article.CurrentVersion = &ver
|
||||||
|
res = append(res, article.Article)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchEduArticle(
|
||||||
|
ctx context.Context,
|
||||||
|
dbConn db.ConnOrTx,
|
||||||
|
slug string,
|
||||||
|
t models.EduArticleType,
|
||||||
|
currentUser *models.User,
|
||||||
|
) (*models.EduArticle, error) {
|
||||||
type eduArticleResult struct {
|
type eduArticleResult struct {
|
||||||
Article models.EduArticle `db:"a"`
|
Article models.EduArticle `db:"a"`
|
||||||
CurrentVersion models.EduArticleVersion `db:"v"`
|
CurrentVersion models.EduArticleVersion `db:"v"`
|
||||||
|
@ -240,12 +309,15 @@ func fetchEduArticle(ctx context.Context, dbConn db.ConnOrTx, slug string, t mod
|
||||||
education_article AS a
|
education_article AS a
|
||||||
JOIN education_article_version AS v ON a.current_version = v.id
|
JOIN education_article_version AS v ON a.current_version = v.id
|
||||||
WHERE
|
WHERE
|
||||||
slug = $?
|
a.slug = $?
|
||||||
`,
|
`,
|
||||||
slug,
|
slug,
|
||||||
)
|
)
|
||||||
if t != 0 {
|
if t != 0 {
|
||||||
qb.Add(`AND type = $?`, t)
|
qb.Add(`AND a.type = $?`, t)
|
||||||
|
}
|
||||||
|
if currentUser == nil || !currentUser.CanSeeUnpublishedEducationContent() {
|
||||||
|
qb.Add(`AND NOT a.published`)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := db.QueryOne[eduArticleResult](ctx, dbConn, qb.String(), qb.Args()...)
|
res, err := db.QueryOne[eduArticleResult](ctx, dbConn, qb.String(), qb.Args()...)
|
||||||
|
|
Loading…
Reference in New Issue