Beef up index or something

This commit is contained in:
Ben Visness 2022-07-23 12:10:28 -05:00
parent c9aa3149ef
commit c550f2cd22
11 changed files with 176 additions and 69 deletions

View File

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

View File

@ -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
*/ */

View File

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

View File

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

View File

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

View File

@ -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">&#9998; Edit</a>
<a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">&#10006; Delete</a>
</div>
</div>
{{ end }}
{{ .Article.Content }} {{ .Article.Content }}
{{ end }} {{ end }}

View File

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

View File

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

View File

@ -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 {

View File

@ -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(),

View File

@ -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()...)