Project index

This commit is contained in:
Asaf Gartner 2021-06-07 02:48:43 +03:00
parent bf96c0bebb
commit b6c611004c
14 changed files with 544 additions and 54 deletions

View File

@ -32,10 +32,6 @@ func TestHomepage(t *testing.T) {
AssertSubdomain(t, BuildProjectHomepage("hero"), "hero")
}
func TestProjectIndex(t *testing.T) {
AssertRegexMatch(t, BuildProjectIndex(), RegexProjectIndex, nil)
}
func TestShowcase(t *testing.T) {
AssertRegexMatch(t, BuildShowcase(), RegexShowcase, nil)
}
@ -70,6 +66,10 @@ func TestLogoutAction(t *testing.T) {
AssertRegexMatch(t, BuildLogoutAction(), RegexLogoutAction, nil)
}
func TestRegister(t *testing.T) {
AssertRegexMatch(t, BuildRegister(), RegexRegister, nil)
}
func TestStaticPages(t *testing.T) {
AssertRegexMatch(t, BuildManifesto(), RegexManifesto, nil)
AssertRegexMatch(t, BuildAbout(), RegexAbout, nil)
@ -94,6 +94,20 @@ func TestFeed(t *testing.T) {
assert.Panics(t, func() { BuildFeedWithPage(0) })
}
func TestProjectIndex(t *testing.T) {
AssertRegexMatch(t, BuildProjectIndex(1), RegexProjectIndex, nil)
AssertRegexMatch(t, BuildProjectIndex(2), RegexProjectIndex, map[string]string{"page": "2"})
assert.Panics(t, func() { BuildProjectIndex(0) })
}
func TestProjectNew(t *testing.T) {
AssertRegexMatch(t, BuildProjectNew(), RegexProjectNew, nil)
}
func TestProjectNotApproved(t *testing.T) {
AssertRegexMatch(t, BuildProjectNotApproved("test"), RegexProjectNotApproved, map[string]string{"slug": "test"})
}
func TestPodcast(t *testing.T) {
AssertRegexMatch(t, BuildPodcast(""), RegexPodcast, nil)
AssertSubdomain(t, BuildPodcast(""), "")

View File

@ -16,8 +16,6 @@ Any function in this package whose name starts with Build is required to be cove
This helps ensure that we don't generate URLs that can't be routed.
*/
// TODO(asaf): Make this whole file only crash in Dev
var RegexHomepage = regexp.MustCompile("^/$")
func BuildHomepage() string {
@ -29,13 +27,6 @@ func BuildProjectHomepage(projectSlug string) string {
return ProjectUrl("/", nil, projectSlug)
}
var RegexProjectIndex = regexp.MustCompile("^/projects$")
func BuildProjectIndex() string {
defer CatchPanic()
return Url("/projects", nil)
}
var RegexShowcase = regexp.MustCompile("^/showcase$")
func BuildShowcase() string {
@ -80,6 +71,13 @@ func BuildLogoutAction() string {
return Url("/logout", nil)
}
var RegexRegister = regexp.MustCompile("^/_register$")
func BuildRegister() string {
defer CatchPanic()
return Url("/_register", nil)
}
/*
* Static Pages
*/
@ -186,6 +184,40 @@ func BuildAtomFeedForShowcase() string {
return Url("/atom/showcase", nil)
}
/*
* Projects
*/
var RegexProjectIndex = regexp.MustCompile("^/projects(/(?P<page>.+)?)?$")
func BuildProjectIndex(page int) string {
defer CatchPanic()
if page < 1 {
panic(oops.New(nil, "page must be >= 1"))
}
if page == 1 {
return Url("/projects", nil)
} else {
return Url(fmt.Sprintf("/projects/%d", page), nil)
}
}
var RegexProjectNew = regexp.MustCompile("^/projects/new$")
func BuildProjectNew() string {
defer CatchPanic()
return Url("/projects/new", nil)
}
var RegexProjectNotApproved = regexp.MustCompile("^/p/(?P<slug>.+)$")
func BuildProjectNotApproved(slug string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/p/%s", slug), nil)
}
/*
* Podcast
*/

View File

@ -24,6 +24,16 @@ const (
ProjectLifecycleLTS
)
// NOTE(asaf): Just checking the lifecycle is not sufficient. Visible projects also must have flags = 0.
var VisibleProjectLifecycles = []ProjectLifecycle{
ProjectLifecycleActive,
ProjectLifecycleHiatus,
ProjectLifecycleLTSRequired, // NOTE(asaf): LTS means complete
ProjectLifecycleLTS,
}
const RecentProjectUpdateTimespanSec = 60 * 60 * 24 * 28 // NOTE(asaf): Four weeks
type Project struct {
ID int `db:"id"`
@ -33,6 +43,7 @@ type Project struct {
Name string `db:"name"`
Blurb string `db:"blurb"`
Description string `db:"description"`
ParsedDescription string `db:"descparsed"`
Lifecycle ProjectLifecycle `db:"lifecycle"` // TODO(asaf): Ensure we only fetch projects in the correct lifecycle phase everywhere.
@ -42,6 +53,8 @@ type Project struct {
LogoLight string `db:"logolight"`
LogoDark string `db:"logodark"`
Flags int `db:"flags"` // NOTE(asaf): Flags is currently only used to mark a project as hidden. Flags == 1 means hidden. Flags == 0 means visible.
Featured bool `db:"featured"`
DateApproved time.Time `db:"date_approved"`
AllLastUpdated time.Time `db:"all_last_updated"`
}

View File

@ -51,17 +51,50 @@ func (p *Post) AddUrls(projectSlug string, subforums []string, threadId int, pos
p.QuoteUrl = hmnurl.BuildForumPostQuote(projectSlug, subforums, threadId, postId)
}
func ProjectToTemplate(p *models.Project) Project {
var LifecycleBadgeClasses = map[models.ProjectLifecycle]string{
models.ProjectLifecycleUnapproved: "",
models.ProjectLifecycleApprovalRequired: "",
models.ProjectLifecycleActive: "",
models.ProjectLifecycleHiatus: "notice-hiatus",
models.ProjectLifecycleDead: "notice-dead",
models.ProjectLifecycleLTSRequired: "",
models.ProjectLifecycleLTS: "notice-lts",
}
var LifecycleBadgeStrings = map[models.ProjectLifecycle]string{
models.ProjectLifecycleUnapproved: "",
models.ProjectLifecycleApprovalRequired: "",
models.ProjectLifecycleActive: "",
models.ProjectLifecycleHiatus: "On Hiatus",
models.ProjectLifecycleDead: "Dead",
models.ProjectLifecycleLTSRequired: "",
models.ProjectLifecycleLTS: "Complete",
}
func ProjectToTemplate(p *models.Project, theme string) Project {
logo := p.LogoLight
if theme == "dark" {
logo = p.LogoDark
}
var url string
if p.Lifecycle == models.ProjectLifecycleUnapproved || p.Lifecycle == models.ProjectLifecycleApprovalRequired {
url = hmnurl.BuildProjectNotApproved(p.Slug)
} else {
url = hmnurl.BuildProjectHomepage(p.Slug)
}
return Project{
Name: p.Name,
Subdomain: p.Subdomain(),
Color1: p.Color1,
Color2: p.Color2,
Url: hmnurl.BuildProjectHomepage(p.Slug),
Url: url,
Blurb: p.Blurb,
ParsedDescription: template.HTML(p.ParsedDescription),
LogoLight: hmnurl.BuildUserFile(p.LogoLight),
LogoDark: hmnurl.BuildUserFile(p.LogoDark),
Logo: hmnurl.BuildUserFile(logo),
LifecycleBadgeClass: LifecycleBadgeClasses[p.Lifecycle],
LifecycleString: LifecycleBadgeStrings[p.Lifecycle],
IsHMN: p.IsHMN(),

View File

@ -37,7 +37,7 @@
<uri>{{ .ProfileUrl }}</uri>
</author>
{{ end }}
<logo>{{ .LogoLight }}</logo>
<logo>{{ .Logo }}</logo>
<summary type="html">{{ .Blurb }}</summary>
</entry>
{{ end }}

View File

@ -1,5 +1,4 @@
<header class="mb3">
{{/* TODO: All the URLs in here are wrong. */}}
<div class="user-options flex justify-center justify-end-ns">
{{ if .User }}
{{ if .User.IsSuperuser }}
@ -31,19 +30,22 @@
</div>
{{ end }}
</div>
<div class="menu-bar flex flex-column flex-row-l justify-between {% if project and project.slug != 'hmn' %}project{% endif %}">
<div class="menu-bar flex flex-column flex-row-l justify-between {{ if .IsProjectPage }}project{{ end }}">
<div class="flex flex-column flex-row-ns">
<a href="{{ .Header.HMNHomepageUrl }}" class="logo hmdev-logo">
<div class="underscore"></div>
</a>
<div class="items flex items-center justify-center justify-start-ns">
{{ if not .Project.IsHMN }}
{{ if .IsProjectPage }}
<a class="project-logo" href="{{ .Header.ProjectHomepageUrl }}">
<h1>{{ .Project.Name }}</h1>
</a>
{{ end }}
{{ if not .IsProjectPage }}
<a href="{{ .Header.ProjectIndexUrl }}" class="projects">Projects</a>
{{ end }}
{{ if .Project.HasBlog }}
<a href="{{ .Header.BlogUrl }}" class="blog">Blog</a>
<a href="{{ .Header.BlogUrl }}" class="blog">{{ if .IsProjectPage }}Blog{{ else }}News{{ end }}</a>
{{ end }}
{{ if .Project.HasForum }}
<a href="{{ .Header.ForumsUrl }}" class="forums">Forums</a>

View File

@ -0,0 +1,16 @@
{{- /*gotype: git.handmade.network/hmn/hmn/src/templates.ProjectCardData*/ -}}
<a class="project-card flex br2 overflow-hidden items-center relative {{ .Classes }}" href="{{ .Project.Url }}" >
<div class="image-container flex-shrink-0">
<div class="image bg-center cover" style="background-image:url({{.Project.Logo}})"></div>
</div>
<div class="details pa3 flex-grow-1">
<h3 class="mb1">{{ .Project.Name }}</h3>
<div class="blurb">{{ .Project.Blurb }}</div>
<div class="badges mt2">
{{ if .Project.LifecycleString }}
<span class="badge {{ .Project.LifecycleBadgeClass }}">{{ .Project.LifecycleString }}</span>
{{ end }}
</div>
</div>
</a>

View File

@ -0,0 +1,152 @@
{{ template "base.html" . }}
{{ define "content" }}
<div class="content-block no-bg-image">
{{ with .CarouselProjects }}
<div class="project-carousel-container mw-100 mv2 mv3-ns margin-center">
<div class="project-carousel pa3 h5 overflow-hidden bg--dim br2-ns">
<div class="dn db-l"> <!-- desktop carousel -->
{{ range $index, $project := . }}
<div class="project-carousel-item flex pa3 w-100 h-100 bg--dim items-center {{ if eq $index 0 }}active{{ end }}">
<div class="flex-grow-1 pr3 relative flex flex-column h-100 justify-center">
<a href="{{ $project.Url }}">
<h3>{{ $project.Name }}</h3>
</a>
<div class="project-carousel-description">
{{ $project.ParsedDescription }}
</div>
<div class="project-carousel-fade"></div>
</div>
<div class="flex-shrink-0 order-0 order-1-ns">
<a href="{{ $project.Url }}">
<div class="image bg-center cover w5 h5 br2" style="background-image:url({{ $project.Logo }})" ></div>
</a>
</div>
</div>
{{ end }}
</div>
<div class="db dn-l"> <!-- mobile/tablet carousel -->
{{ range $index, $project := . }}
<div class="project-carousel-item-small {{ if eq $index 0 }}active{{ end }}">
{{ template "project_card.html" projectcarddata $project "h-100" }}
</div>
{{ end }}
</div>
</div>
<div class="flex justify-center pv2">
{{ range $index, $project := . }}
<div
class="project-carousel-button br-pill w1 h1 mh2 {{ if eq $index 0 }}active{{ end }}"
onclick="carouselButtonClick({{ $index }})"
></div>
{{ end }}
</div>
</div>
{{ end }}
<div class="flex flex-column flex-row-l mv3 items-start">
<div class="bg--dim-ns br2">
<div class="clear"></div>
<div class="optionbar pv2 ph3">
<a href="{{ .ProjectAtomFeedUrl }}"><span class="icon big">4</span> RSS Feed &ndash; New Projects</span></a>
{{ template "pagination.html" .Pagination }}
</div>
<div class="projectlist ph3">
{{ range .Projects }}
<div class="mv3">
{{ template "project_card.html" projectcarddata . ""}}
</div>
{{ end }}
</div>
<div class="optionbar bottom pv2 ph3">
<div class="options order-1"></div>
<div class="options order-0 order-last-ns">{{ template "pagination.html" .Pagination }}</div>
</div>
</div>
<div class="w-100 w-40-l ph3 ph0-ns flex-shrink-0">
<div class="ml3-l mt3 mt0-l pa3 bg--dim br2">
{{ if not .UserPendingProject }}
<div class="content-block new-project p-spaced">
<h2>Submit a Project</h2>
<pre>your-project-here.handmade.network</pre>
<p>
Developing something cool? Building a tool, game, website, etc.
that meshes with the <a href="{{ .ManifestoUrl }}">Handmade philosophy</a>?
</p>
{{ if .User }}
<p class="center">
<a class="button" href="{{ .NewProjectUrl }}">Submit a Project</a>
</p>
{{ else }}
<p>
Become a member now to submit a project to the site!
</p>
<p class="center">
<a class="button" href="{{ .RegisterUrl }}">Register</a>
<span class="mh2">or</span>
<a class="button" href="{{ .LoginUrl }}">Log In</a>
</p>
{{ end }}
</div>
{{ else }}
<div class="content-block single">
<h2>Project pending</h2>
<p>Thanks for considering us as a home for<br /><a href="{{ .UserPendingProject.Url }}">{{ .UserPendingProject.Name }}</a>!</p>
<br />
{{ if .UserPendingProjectUnderReview }}
<p>We see it's ready for review by an administrator, great! We'll try and get back to you in a timely manner.</p>
{{ else }}
<p>When you're ready for us to review it, let us know using the checkbox on {{ .UserPendingProject.Name }}'s profile editor.</p>
{{ end }}
</div>
{{ end }}
{{ if .UserApprovedProjects }}
<div class="content-block single projectlist">
{{ if .UserPendingProject }}
<h2>Your other projects</h2>
{{ else }}
<h2>Your projects</h2>
{{ end }}
{{ range .UserApprovedProjects }}
<div class="mv3">
{{ template "project_card.html" projectcarddata . "" }}
</div>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
</div>
<script>
const numCarouselItems = {{ len .CarouselProjects }};
function activateCarouselProject(i) {
const items = document.querySelectorAll('.project-carousel-item');
items.forEach(item => item.classList.remove('active'));
items[i].classList.add('active');
const smallItems = document.querySelectorAll('.project-carousel-item-small');
smallItems.forEach(item => item.classList.remove('active'));
smallItems[i].classList.add('active');
const buttons = document.querySelectorAll('.project-carousel-button');
buttons.forEach(button => button.classList.remove('active'));
buttons[i].classList.add('active');
}
let carouselTimerCurrent = 0;
const carouselTimer = setInterval(() => {
const next = (carouselTimerCurrent + 1) % numCarouselItems;
activateCarouselProject(next);
carouselTimerCurrent = next;
}, 10000);
function carouselButtonClick(i) {
activateCarouselProject(i);
clearInterval(carouselTimer);
}
</script>
{{ end }}

View File

@ -158,6 +158,14 @@ var HMNTemplateFuncs = template.FuncMap{
"noescape": func(str string) template.HTML {
return template.HTML(str)
},
// NOTE(asaf): Template specific functions:
"projectcarddata": func(project Project, classes string) ProjectCardData {
return ProjectCardData{
Project: &project,
Classes: classes,
}
},
}
type ErrInvalidHexColor struct {

View File

@ -20,6 +20,7 @@ type BaseData struct {
Project Project
User *User
IsProjectPage bool
Header Header
Footer Footer
}
@ -32,6 +33,7 @@ type Header struct {
RegisterUrl string
HMNHomepageUrl string
ProjectHomepageUrl string
ProjectIndexUrl string
BlogUrl string
ForumsUrl string
WikiUrl string
@ -92,10 +94,13 @@ type Project struct {
Color2 string
Url string
Blurb string
ParsedDescription template.HTML
Owners []User
LogoDark string
LogoLight string
Logo string
LifecycleBadgeClass string
LifecycleString string
IsHMN bool
@ -192,6 +197,11 @@ type ThreadListItem struct {
Content string
}
type ProjectCardData struct {
Project *Project
Classes string
}
type Breadcrumb struct {
Name, Url string
Current bool

View File

@ -183,7 +183,7 @@ func AtomFeed(c *RequestContext) ResponseData {
feedData.FeedType = FeedTypeProjects
feedData.FeedID = FeedIDProjects
feedData.AtomFeedUrl = hmnurl.BuildAtomFeedForProjects()
feedData.FeedUrl = hmnurl.BuildProjectIndex()
feedData.FeedUrl = hmnurl.BuildProjectIndex(1)
c.Perf.StartBlock("SQL", "Fetching projects")
type projectResult struct {
@ -192,10 +192,15 @@ func AtomFeed(c *RequestContext) ResponseData {
projects, err := db.Query(c.Context(), c.Conn, projectResult{},
`
SELECT $columns
FROM handmade_project AS project
FROM
handmade_project AS project
WHERE
project.lifecycle = ANY($1)
AND project.flags = 0
ORDER BY date_approved DESC
LIMIT $1
LIMIT $2
`,
models.VisibleProjectLifecycles,
itemsPerFeed,
)
if err != nil {
@ -205,7 +210,7 @@ func AtomFeed(c *RequestContext) ResponseData {
projectMap := make(map[int]*templates.Project)
for _, p := range projects.ToSlice() {
project := p.(*projectResult).Project
templateProject := templates.ProjectToTemplate(&project)
templateProject := templates.ProjectToTemplate(&project, c.Theme)
templateProject.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(templateProject.Url)).URN()
projectIds = append(projectIds, project.ID)

View File

@ -55,12 +55,12 @@ func Index(c *RequestContext) ResponseData {
SELECT $columns
FROM handmade_project
WHERE
(flags = 0 AND NOT lifecycle = ANY($1))
(flags = 0 AND lifecycle = ANY($1))
OR id = $2
ORDER BY all_last_updated DESC
LIMIT $3
`,
[]models.ProjectLifecycle{models.ProjectLifecycleUnapproved, models.ProjectLifecycleApprovalRequired},
models.VisibleProjectLifecycles,
models.HMNProjectID,
numProjectsToGet*2, // hedge your bets against projects that don't have any content
)
@ -140,7 +140,7 @@ func Index(c *RequestContext) ResponseData {
}
landingPageProject := LandingPageProject{
Project: templates.ProjectToTemplate(proj),
Project: templates.ProjectToTemplate(proj, c.Theme),
ForumsUrl: forumsUrl,
}

203
src/website/projects.go Normal file
View File

@ -0,0 +1,203 @@
package website
import (
"math"
"math/rand"
"net/http"
"strconv"
"time"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils"
)
type ProjectTemplateData struct {
templates.BaseData
Pagination templates.Pagination
CarouselProjects []templates.Project
Projects []templates.Project
UserPendingProjectUnderReview bool
UserPendingProject *templates.Project
UserApprovedProjects []templates.Project
ProjectAtomFeedUrl string
ManifestoUrl string
NewProjectUrl string
RegisterUrl string
LoginUrl string
}
func ProjectIndex(c *RequestContext) ResponseData {
const projectsPerPage = 20
const maxCarouselProjects = 10
page := 1
pageString, hasPage := c.PathParams["page"]
if hasPage && pageString != "" {
if pageParsed, err := strconv.Atoi(pageString); err == nil {
page = pageParsed
} else {
return c.Redirect(hmnurl.BuildProjectIndex(1), http.StatusSeeOther)
}
}
if page < 1 {
return c.Redirect(hmnurl.BuildProjectIndex(1), http.StatusSeeOther)
}
c.Perf.StartBlock("SQL", "Fetching all visible projects")
type projectResult struct {
Project models.Project `db:"project"`
}
allProjects, err := db.Query(c.Context(), c.Conn, projectResult{},
`
SELECT $columns
FROM
handmade_project AS project
WHERE
project.lifecycle = ANY($1)
AND project.flags = 0
ORDER BY project.date_approved ASC
`,
models.VisibleProjectLifecycles,
)
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch projects"))
}
allProjectsSlice := allProjects.ToSlice()
c.Perf.EndBlock()
numPages := int(math.Ceil(float64(len(allProjectsSlice)) / projectsPerPage))
if page > numPages {
return c.Redirect(hmnurl.BuildProjectIndex(numPages), http.StatusSeeOther)
}
pagination := templates.Pagination{
Current: page,
Total: numPages,
FirstUrl: hmnurl.BuildProjectIndex(1),
LastUrl: hmnurl.BuildProjectIndex(numPages),
NextUrl: hmnurl.BuildProjectIndex(utils.IntClamp(1, page+1, numPages)),
PreviousUrl: hmnurl.BuildProjectIndex(utils.IntClamp(1, page-1, numPages)),
}
var userApprovedProjects []templates.Project
var userPendingProject *templates.Project
userPendingProjectUnderReview := false
if c.CurrentUser != nil {
c.Perf.StartBlock("SQL", "fetching user projects")
type UserProjectQuery struct {
Project models.Project `db:"project"`
}
userProjectsResult, err := db.Query(c.Context(), c.Conn, UserProjectQuery{},
`
SELECT $columns
FROM
handmade_project AS project
INNER JOIN handmade_project_groups AS project_groups ON project_groups.project_id = project.id
INNER JOIN auth_user_groups AS user_groups ON user_groups.group_id = project_groups.group_id
WHERE
user_groups.user_id = $1
`,
c.CurrentUser.ID,
)
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user projects"))
}
for _, project := range userProjectsResult.ToSlice() {
p := project.(*UserProjectQuery).Project
if p.Lifecycle == models.ProjectLifecycleUnapproved || p.Lifecycle == models.ProjectLifecycleApprovalRequired {
if userPendingProject == nil {
// NOTE(asaf): Technically a user could have more than one pending project.
// For example, if they created one project themselves and were added as an additional owner to another user's project.
// So we'll just take the first one. I don't think it matters. I guess it especially won't matter after Projects 2.0.
tmplProject := templates.ProjectToTemplate(&p, c.Theme)
userPendingProject = &tmplProject
userPendingProjectUnderReview = (p.Lifecycle == models.ProjectLifecycleApprovalRequired)
}
} else {
userApprovedProjects = append(userApprovedProjects, templates.ProjectToTemplate(&p, c.Theme))
}
}
c.Perf.EndBlock()
}
c.Perf.StartBlock("PROJECTS", "Grouping and sorting")
var handmadeHero *templates.Project
var featuredProjects []templates.Project
var recentProjects []templates.Project
var restProjects []templates.Project
now := time.Now()
for _, p := range allProjectsSlice {
project := &p.(*projectResult).Project
templateProject := templates.ProjectToTemplate(project, c.Theme)
if project.Slug == "hero" {
// NOTE(asaf): Handmade Hero gets special treatment. Must always be first in the list.
handmadeHero = &templateProject
continue
}
if project.Featured {
featuredProjects = append(featuredProjects, templateProject)
} else if now.Sub(project.AllLastUpdated).Seconds() < models.RecentProjectUpdateTimespanSec {
recentProjects = append(recentProjects, templateProject)
} else {
restProjects = append(restProjects, templateProject)
}
}
_, randSeed := now.ISOWeek()
random := rand.New(rand.NewSource(int64(randSeed)))
random.Shuffle(len(featuredProjects), func(i, j int) { featuredProjects[i], featuredProjects[j] = featuredProjects[j], featuredProjects[i] })
random.Shuffle(len(recentProjects), func(i, j int) { recentProjects[i], recentProjects[j] = recentProjects[j], recentProjects[i] })
random.Shuffle(len(restProjects), func(i, j int) { restProjects[i], restProjects[j] = restProjects[j], restProjects[i] })
if handmadeHero != nil {
// NOTE(asaf): As mentioned above, inserting HMH first.
featuredProjects = append([]templates.Project{*handmadeHero}, featuredProjects...)
}
orderedProjects := make([]templates.Project, 0, len(featuredProjects)+len(recentProjects)+len(restProjects))
orderedProjects = append(orderedProjects, featuredProjects...)
orderedProjects = append(orderedProjects, recentProjects...)
orderedProjects = append(orderedProjects, restProjects...)
firstProjectIndex := (page - 1) * projectsPerPage
endIndex := utils.IntMin(firstProjectIndex+projectsPerPage, len(orderedProjects))
pageProjects := orderedProjects[firstProjectIndex:endIndex]
var carouselProjects []templates.Project
if page == 1 {
carouselProjects = featuredProjects[:utils.IntMin(len(featuredProjects), maxCarouselProjects)]
}
c.Perf.EndBlock()
baseData := getBaseData(c)
baseData.Title = "Project List"
var res ResponseData
res.WriteTemplate("project_index.html", ProjectTemplateData{
BaseData: baseData,
Pagination: pagination,
CarouselProjects: carouselProjects,
Projects: pageProjects,
UserPendingProjectUnderReview: userPendingProjectUnderReview,
UserPendingProject: userPendingProject,
UserApprovedProjects: userApprovedProjects,
ProjectAtomFeedUrl: hmnurl.BuildAtomFeedForProjects(),
ManifestoUrl: hmnurl.BuildManifesto(),
NewProjectUrl: hmnurl.BuildProjectNew(),
RegisterUrl: hmnurl.BuildRegister(),
LoginUrl: hmnurl.BuildLoginPage(c.FullUrl()),
}, c.Perf)
return res
}

View File

@ -106,9 +106,9 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
mainRoutes.GET(hmnurl.RegexFeed, Feed)
mainRoutes.GET(hmnurl.RegexAtomFeed, AtomFeed)
// TODO(asaf): Trailing slashes break these
mainRoutes.GET(hmnurl.RegexProjectIndex, ProjectIndex)
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
// mainRoutes.GET(`^/(?P<cats>forums(/cat)*)/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`, ForumPost)
mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory)
mainRoutes.GET(hmnurl.RegexProjectCSS, ProjectCSS)
@ -131,11 +131,12 @@ func getBaseData(c *RequestContext) templates.BaseData {
}
return templates.BaseData{
Project: templates.ProjectToTemplate(c.CurrentProject),
Project: templates.ProjectToTemplate(c.CurrentProject, c.Theme),
LoginPageUrl: hmnurl.BuildLoginPage(c.FullUrl()),
User: templateUser,
Theme: c.Theme,
ProjectCSSUrl: hmnurl.BuildProjectCSS(c.CurrentProject.Color1),
IsProjectPage: !c.CurrentProject.IsHMN(),
Header: templates.Header{
AdminUrl: hmnurl.BuildHomepage(), // TODO(asaf)
MemberSettingsUrl: hmnurl.BuildHomepage(), // TODO(asaf)
@ -144,6 +145,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
RegisterUrl: hmnurl.BuildHomepage(), // TODO(asaf)
HMNHomepageUrl: hmnurl.BuildHomepage(), // TODO(asaf)
ProjectHomepageUrl: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
BlogUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, 1),
ForumsUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1),
WikiUrl: hmnurl.BuildWiki(c.CurrentProject.Slug),
@ -159,7 +161,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
ManifestoUrl: hmnurl.BuildManifesto(),
CodeOfConductUrl: hmnurl.BuildCodeOfConduct(),
CommunicationGuidelinesUrl: hmnurl.BuildCommunicationGuidelines(),
ProjectIndexUrl: hmnurl.BuildProjectIndex(),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
ForumsUrl: hmnurl.BuildForumCategory(models.HMNProjectSlug, nil, 1),
ContactUrl: hmnurl.BuildContactPage(),
SitemapUrl: hmnurl.BuildSiteMap(),