Rework structure of project index. Need new copy.

This commit is contained in:
Ben Visness 2023-09-25 02:40:45 -05:00
parent 45b4928d83
commit 25cc5ef11b
9 changed files with 349 additions and 130 deletions

View File

@ -7553,6 +7553,15 @@ article code {
.g5 {
gap: 4rem; }
.grid {
display: grid; }
.grid-1 {
grid-template-columns: 1fr; }
.grid-2 {
grid-template-columns: 1fr 1fr; }
.aspect-ratio--2x1 {
padding-bottom: 50%; }
@ -7582,6 +7591,10 @@ article code {
column-gap: 2rem; }
.cg5-ns {
column-gap: 4rem; }
.grid-1-ns {
grid-template-columns: 1fr; }
.grid-2-ns {
grid-template-columns: 1fr 1fr; }
.bg--dim-ns {
background-color: #f0f0f0;
background-color: var(--dim-background); } }
@ -7607,6 +7620,10 @@ article code {
column-gap: 2rem; }
.cg5-m {
column-gap: 4rem; }
.grid-1-m {
grid-template-columns: 1fr; }
.grid-2-m {
grid-template-columns: 1fr 1fr; }
.bg--dim-m {
background-color: #f0f0f0;
background-color: var(--dim-background); } }
@ -7632,6 +7649,10 @@ article code {
column-gap: 2rem; }
.cg5-l {
column-gap: 4rem; }
.grid-1-l {
grid-template-columns: 1fr; }
.grid-2-l {
grid-template-columns: 1fr 1fr; }
.bg--dim-l {
background-color: #f0f0f0;
background-color: var(--dim-background); } }

View File

@ -19,6 +19,7 @@ type Jam struct {
Event
Name string
Slug string
UrlSlug string
}
var WRJ2021 = Jam{
@ -28,6 +29,7 @@ var WRJ2021 = Jam{
},
Name: "Wheel Reinvention Jam 2021",
Slug: "WRJ2021",
UrlSlug: "2021",
}
var WRJ2022 = Jam{
@ -37,6 +39,7 @@ var WRJ2022 = Jam{
},
Name: "Wheel Reinvention Jam 2022",
Slug: "WRJ2022",
UrlSlug: "2022",
}
var VJ2023 = Jam{
@ -46,6 +49,7 @@ var VJ2023 = Jam{
},
Name: "Visibility Jam 2023",
Slug: "VJ2023",
UrlSlug: "visibility-2023",
}
var WRJ2023 = Jam{
@ -55,6 +59,7 @@ var WRJ2023 = Jam{
},
Name: "Wheel Reinvention Jam 2023",
Slug: "WRJ2023",
UrlSlug: "2023",
}
var HMS2022 = Event{
@ -84,6 +89,17 @@ func CurrentJam() *Jam {
return nil
}
func PreviousJam() *Jam {
now := time.Now()
var mostRecent *Jam
for i, jam := range AllJams {
if jam.EndTime.Before(now) {
mostRecent = &AllJams[i]
}
}
return mostRecent
}
func JamBySlug(slug string) Jam {
for _, jam := range AllJams {
if jam.Slug == slug {

View File

@ -142,9 +142,11 @@ func TestFeed(t *testing.T) {
}
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) })
AssertRegexMatch(t, BuildProjectIndex(1, ""), RegexProjectIndex, nil)
AssertRegexMatch(t, BuildProjectIndex(2, ""), RegexProjectIndex, map[string]string{"page": "2"})
AssertRegexMatch(t, BuildProjectIndex(1, "test"), RegexProjectIndex, map[string]string{"category": "test"})
AssertRegexMatch(t, BuildProjectIndex(2, "test"), RegexProjectIndex, map[string]string{"page": "2", "category": "test"})
assert.Panics(t, func() { BuildProjectIndex(0, "") })
}
func TestProjectNew(t *testing.T) {
@ -415,6 +417,16 @@ func TestJamRecap2023_Visibility(t *testing.T) {
AssertSubdomain(t, BuildJamRecap2023_Visibility(), "")
}
func TestJamIndex2023(t *testing.T) {
AssertRegexMatch(t, BuildJamIndex2023(), RegexJamIndex2023, nil)
AssertSubdomain(t, BuildJamIndex2023(), "")
}
func TestJamFeed2023(t *testing.T) {
AssertRegexMatch(t, BuildJamFeed2023(), RegexJamFeed2023, nil)
AssertSubdomain(t, BuildJamFeed2023(), "")
}
func TestTimeMachine(t *testing.T) {
AssertRegexMatch(t, BuildTimeMachine(), RegexTimeMachine, nil)
AssertSubdomain(t, BuildTimeMachine(), "")

View File

@ -126,6 +126,11 @@ func BuildJamRecap2023_Visibility() string {
return Url("/jam/visibility-2023/recap", nil)
}
func BuildJamIndexAny(slug string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/jam/%s", slug), nil)
}
var RegexTimeMachine = regexp.MustCompile("^/timemachine$")
func BuildTimeMachine() string {
@ -445,17 +450,21 @@ func BuildAtomFeedForShowcase() string {
* Projects
*/
var RegexProjectIndex = regexp.MustCompile("^/projects(/(?P<page>.+)?)?$")
var RegexProjectIndex = regexp.MustCompile(`^/projects(/(?P<category>[a-z][a-z0-9]+))?(/(?P<page>\d+))?$`)
func BuildProjectIndex(page int) string {
func BuildProjectIndex(page int, category string) string {
defer CatchPanic()
if page < 1 {
panic(oops.New(nil, "page must be >= 1"))
}
catpath := ""
if category != "" {
catpath = "/" + category
}
if page == 1 {
return Url("/projects", nil)
return Url(fmt.Sprintf("/projects%s", catpath), nil)
} else {
return Url(fmt.Sprintf("/projects/%d", page), nil)
return Url(fmt.Sprintf("/projects%s/%d", catpath, page), nil)
}
}

View File

@ -372,6 +372,18 @@ article code {
.g4 { gap: $spacing-large; }
.g5 { gap: $spacing-extra-large; }
.grid {
display: grid;
}
.grid-1 {
grid-template-columns: 1fr;
}
.grid-2 {
grid-template-columns: 1fr 1fr;
}
.aspect-ratio--2x1 {
padding-bottom: 50%;
}
@ -392,6 +404,8 @@ article code {
.cg3-ns { column-gap: $spacing-medium; }
.cg4-ns { column-gap: $spacing-large; }
.cg5-ns { column-gap: $spacing-extra-large; }
.grid-1-ns { grid-template-columns: 1fr; }
.grid-2-ns { grid-template-columns: 1fr 1fr; }
.bg--dim-ns {
@include usevar(background-color, dim-background);
@ -409,6 +423,8 @@ article code {
.cg3-m { column-gap: $spacing-medium; }
.cg4-m { column-gap: $spacing-large; }
.cg5-m { column-gap: $spacing-extra-large; }
.grid-1-m { grid-template-columns: 1fr; }
.grid-2-m { grid-template-columns: 1fr 1fr; }
.bg--dim-m {
@include usevar(background-color, dim-background);
@ -426,6 +442,8 @@ article code {
.cg3-l { column-gap: $spacing-medium; }
.cg4-l { column-gap: $spacing-large; }
.cg5-l { column-gap: $spacing-extra-large; }
.grid-1-l { grid-template-columns: 1fr; }
.grid-2-l { grid-template-columns: 1fr 1fr; }
.bg--dim-l {
@include usevar(background-color, dim-background);

View File

@ -4,13 +4,13 @@
<script src="{{ static "js/carousel.js" }}"></script>
{{ end }}
{{ define "content" }}
{{ define "all_projects" }}
<div>
{{ with .CarouselProjects }}
{{ with .OfficialProjects }}
<div class="carousel-container project-carousel mw-100 mv2 mv3-ns margin-center dn db-ns">
<div class="carousel pa3 h5 overflow-hidden bg--dim br2-ns">
{{ range $index, $project := . }}
<div class="carousel-item flex pa3 w-100 h-100 bg--dim items-center {{ if eq $index 0 }}active{{ end }}">
<div class="carousel-item flex pv3 pl3 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>
@ -31,42 +31,55 @@
<div class="carousel-buttons pv2"></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">
<div class="options">
<a href="{{ .ProjectAtomFeedUrl }}"><span class="icon big">4</span> RSS Feed &ndash; New Projects</span></a>
</div>
<div class="options">
{{ template "pagination.html" .Pagination }}
</div>
</div>
<div class="projectlist ph3">
{{ range .Projects }}
<div class="mv3">
<div class="flex flex-column g3">
{{ if .OfficialProjects }}
<div class="ph3 pt3 bg--dim br2 flex flex-column">
<h2>Official Projects</h2>
<p>These projects are boop boop hahahaa:</p>
<div class="grid grid-1 grid-2-ns g3">
{{ range .OfficialProjects }}
{{ 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>
<a href="{{ .OfficialProjectsLink }}" class="pa3 tc">See more »</a>
</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">
{{ end }}
{{ if .PersonalProjects }}
<div class="ph3 pt3 bg--dim br2 flex flex-column">
<h2>Personal Projects</h2>
<p>Many community members have projects of their own that are currently works in progress. Here's a few:</p>
<p>Many community members have projects of their own. Here's a few:</p>
<div class="grid grid-1 grid-2-ns g3">
{{ range .PersonalProjects }}
<div class="mv3">
{{ template "project_card.html" projectcarddata . "" }}
</div>
{{ end }}
</div>
<a href="{{ .PersonalProjectsLink }}" class="pa3 tc">See more »</a>
</div>
{{ end }}
{{ if .CurrentJamProjects }}
<div class="ph3 pt3 bg--dim br2 flex flex-column">
<h2>JAM! NOWS!</h2>
<p>wowowowow</p>
<div class="grid grid-1 grid-2-ns g3">
{{ range .CurrentJamProjects }}
{{ template "project_card.html" projectcarddata . "" }}
{{ end }}
</div>
<a href="{{ .CurrentJamLink }}" class="pa3 tc">See more »</a>
</div>
{{ end }}
{{ if .PreviousJamProjects }}
<div class="ph3 pt3 bg--dim br2 flex flex-column">
<h2>JAM! THENS!</h2>
<p>nonononoono</p>
<div class="grid grid-1 grid-2-ns g3">
{{ range .PreviousJamProjects }}
{{ template "project_card.html" projectcarddata . "" }}
{{ end }}
</div>
<a href="{{ .PreviousJamLink }}" class="pa3 tc">See more »</a>
</div>
{{ end }}
</div>
</div>
@ -76,3 +89,34 @@
});
</script>
{{ end }}
{{ define "single_category" }}
<div class="bg--dim-ns br2">
<div class="clear"></div>
<div class="optionbar pv2 ph3">
<div class="options"></div>
<div class="options">
{{ template "pagination.html" .Pagination }}
</div>
</div>
<div class="projectlist pa3 grid grid-1 grid-2-ns g3">
{{ range .PageProjects }}
{{ template "project_card.html" projectcarddata . "" }}
{{ 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>
{{ end }}
{{ define "content" }}
{{ if .AllProjects }}
{{ template "all_projects" . }}
{{ else }}
{{ template "single_category" . }}
{{ end }}
{{ end }}

View File

@ -72,7 +72,7 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
LoginWithDiscordUrl: hmnurl.BuildLoginWithDiscord(c.FullUrl()),
HMNHomepageUrl: hmnurl.BuildHomepage(),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1, ""),
PodcastUrl: hmnurl.BuildPodcast(),
FishbowlUrl: hmnurl.BuildFishbowlIndex(),
ForumsUrl: hmnurl.HMNProjectContext.BuildForum(nil, 1),
@ -85,7 +85,7 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
AboutUrl: hmnurl.BuildAbout(),
ManifestoUrl: hmnurl.BuildManifesto(),
CommunicationGuidelinesUrl: hmnurl.BuildCommunicationGuidelines(),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
ProjectIndexUrl: hmnurl.BuildProjectIndex(1, ""),
RolesUrl: hmnurl.BuildStaffRolesIndex(),
ContactUrl: hmnurl.BuildContactPage(),
SearchActionUrl: "https://duckduckgo.com",

View File

@ -149,7 +149,7 @@ func AtomFeed(c *RequestContext) ResponseData {
feedData.FeedType = FeedTypeProjects
feedData.FeedID = FeedIDProjects
feedData.AtomFeedUrl = hmnurl.BuildAtomFeedForProjects()
feedData.FeedUrl = hmnurl.BuildProjectIndex(1)
feedData.FeedUrl = hmnurl.BuildProjectIndex(1, "")
c.Perf.StartBlock("SQL", "Fetching projects")
_, hasAll := c.Req.URL.Query()["all"]

View File

@ -6,7 +6,6 @@ import (
"fmt"
"image"
"io"
"math"
"math/rand"
"net/http"
"path"
@ -29,7 +28,7 @@ import (
"github.com/teacat/noire"
)
const maxPersonalProjects = 5
const maxPersonalProjects = 10
const maxProjectOwners = 5
func ProjectCSS(c *RequestContext) ResponseData {
@ -75,41 +74,143 @@ func ProjectCSS(c *RequestContext) ResponseData {
type ProjectTemplateData struct {
templates.BaseData
Pagination templates.Pagination
CarouselProjects []templates.Project
Projects []templates.Project
PersonalProjects []templates.Project
AllProjects bool
ProjectAtomFeedUrl string
WIPForumUrl string
// Stuff for all projects
OfficialProjects []templates.Project
OfficialProjectsLink string
PersonalProjects []templates.Project
PersonalProjectsLink string
CurrentJamProjects []templates.Project
CurrentJamLink string
PreviousJamProjects []templates.Project
PreviousJamLink string
// Stuff for pages of projects only
Pagination templates.Pagination
PageProjects []templates.Project
}
func ProjectIndex(c *RequestContext) ResponseData {
const projectsPerPage = 20
const maxCarouselProjects = 10
const maxPersonalProjects = 10
cat := c.PathParams["category"]
pageStr := c.PathParams["page"]
officialProjects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
Types: hmndata.OfficialProjects,
})
currentJam := hmndata.CurrentJam()
previousJam := hmndata.PreviousJam()
if cat == "" && pageStr == "" {
const projectsPerSection = 8
officialProjects, err := getShuffledOfficialProjects(c)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch projects"))
return c.ErrorResponse(http.StatusInternalServerError, err)
}
numPages := int(math.Ceil(float64(len(officialProjects)) / projectsPerPage))
page, numPages, ok := getPageInfo(c.PathParams["page"], len(officialProjects), feedPostsPerPage)
personalProjects, err := getPersonalProjects(c, "")
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
var currentJamProjects []templates.Project
if currentJam != nil {
var err error
currentJamProjects, err = getPersonalProjects(c, currentJam.Slug)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
}
previousJamProjects, err := getPersonalProjects(c, previousJam.Slug)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
baseData := getBaseDataAutocrumb(c, "Projects")
tmpl := ProjectTemplateData{
BaseData: baseData,
AllProjects: true,
OfficialProjects: officialProjects[:utils.IntMin(len(officialProjects), projectsPerSection)],
OfficialProjectsLink: hmnurl.BuildProjectIndex(1, "official"),
PersonalProjects: personalProjects[:utils.IntMin(len(personalProjects), projectsPerSection)],
PersonalProjectsLink: hmnurl.BuildProjectIndex(1, "personal"),
CurrentJamProjects: currentJamProjects[:utils.IntMin(len(currentJamProjects), projectsPerSection)],
// CurrentJamLink set later
PreviousJamProjects: previousJamProjects[:utils.IntMin(len(previousJamProjects), projectsPerSection)],
PreviousJamLink: hmnurl.BuildJamIndexAny(previousJam.UrlSlug),
}
if hmndata.CurrentJam() != nil {
tmpl.CurrentJamLink = hmnurl.BuildJamIndexAny(hmndata.CurrentJam().UrlSlug)
}
var res ResponseData
res.MustWriteTemplate("project_index.html", tmpl, c.Perf)
return res
} else {
const projectsPerPage = 20
var projects []templates.Project
var err error
switch cat {
case hmndata.WRJ2022.UrlSlug:
projects, err = getPersonalProjects(c, hmndata.WRJ2022.Slug)
case hmndata.VJ2023.Slug:
projects, err = getPersonalProjects(c, hmndata.VJ2023.Slug)
case hmndata.WRJ2023.Slug:
projects, err = getPersonalProjects(c, hmndata.WRJ2023.Slug)
case "personal":
projects, err = getPersonalProjects(c, "")
case "official":
projects, err = getShuffledOfficialProjects(c)
default:
return c.Redirect(hmnurl.BuildProjectIndex(1, ""), http.StatusSeeOther)
}
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
page, numPages, ok := getPageInfo(pageStr, len(projects), projectsPerPage)
if !ok {
return c.Redirect(hmnurl.BuildProjectIndex(1), http.StatusSeeOther)
return c.Redirect(hmnurl.BuildProjectIndex(1, cat), 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)),
FirstUrl: hmnurl.BuildProjectIndex(1, cat),
LastUrl: hmnurl.BuildProjectIndex(numPages, cat),
NextUrl: hmnurl.BuildProjectIndex(utils.IntClamp(1, page+1, numPages), cat),
PreviousUrl: hmnurl.BuildProjectIndex(utils.IntClamp(1, page-1, numPages), cat),
}
firstProjectIndex := (page - 1) * projectsPerPage
endIndex := utils.IntMin(firstProjectIndex+projectsPerPage, len(projects))
pageProjects := projects[firstProjectIndex:endIndex]
baseData := getBaseData(c, "Projects", []templates.Breadcrumb{
{"Projects", hmnurl.BuildProjectIndex(1, "")},
})
var res ResponseData
res.MustWriteTemplate("project_index.html", ProjectTemplateData{
BaseData: baseData,
AllProjects: false,
Pagination: pagination,
PageProjects: pageProjects,
}, c.Perf)
return res
}
}
func getShuffledOfficialProjects(c *RequestContext) ([]templates.Project, error) {
official, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
Types: hmndata.OfficialProjects,
})
if err != nil {
return nil, oops.New(err, "failed to fetch projects")
}
c.Perf.StartBlock("PROJECTS", "Grouping and sorting")
@ -118,7 +219,7 @@ func ProjectIndex(c *RequestContext) ResponseData {
var recentProjects []templates.Project
var restProjects []templates.Project
now := time.Now()
for _, p := range officialProjects {
for _, p := range official {
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage(), c.Theme)
if p.Project.Slug == "hero" {
@ -146,29 +247,28 @@ func ProjectIndex(c *RequestContext) ResponseData {
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...)
officialProjects := make([]templates.Project, 0, len(featuredProjects)+len(recentProjects)+len(restProjects))
officialProjects = append(officialProjects, featuredProjects...)
officialProjects = append(officialProjects, recentProjects...)
officialProjects = append(officialProjects, 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()
// Fetch and highlight a random selection of personal projects
var personalProjects []templates.Project
{
return officialProjects, nil
}
func getPersonalProjects(c *RequestContext, jamSlug string) ([]templates.Project, error) {
var slugs []string
if jamSlug != "" {
slugs = []string{jamSlug}
}
projects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
Types: hmndata.PersonalProjects,
JamSlugs: slugs,
})
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch personal projects"))
return nil, oops.New(err, "failed to fetch personal projects")
}
sort.Slice(projects, func(i, j int) bool {
@ -177,29 +277,28 @@ func ProjectIndex(c *RequestContext) ResponseData {
return p2.AllLastUpdated.Before(p1.AllLastUpdated) // sort backwards - recent first
})
for i, p := range projects {
if i >= maxPersonalProjects {
break
}
var personalProjects []templates.Project
for _, p := range projects {
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage(), c.Theme)
personalProjects = append(personalProjects, templateProject)
}
return personalProjects, nil
}
func jamLink(jamSlug string) string {
switch jamSlug {
case hmndata.WRJ2021.Slug:
return hmnurl.BuildJamIndex2021()
case hmndata.WRJ2022.Slug:
return hmnurl.BuildJamIndex2022()
case hmndata.WRJ2023.Slug:
return hmnurl.BuildJamIndex2023()
case hmndata.VJ2023.Slug:
return hmnurl.BuildJamIndex2023_Visibility()
default:
return ""
}
baseData := getBaseDataAutocrumb(c, "Projects")
var res ResponseData
res.MustWriteTemplate("project_index.html", ProjectTemplateData{
BaseData: baseData,
Pagination: pagination,
CarouselProjects: carouselProjects,
Projects: pageProjects,
PersonalProjects: personalProjects,
ProjectAtomFeedUrl: hmnurl.BuildAtomFeedForProjects(),
WIPForumUrl: hmnurl.HMNProjectContext.BuildForum([]string{"wip"}, 1),
}, c.Perf)
return res
}
type ProjectHomepageData struct {