2021-06-06 23:48:43 +00:00
package website
import (
2021-07-08 07:40:30 +00:00
"fmt"
2021-06-06 23:48:43 +00:00
"math"
"math/rand"
"net/http"
2021-11-11 20:00:36 +00:00
"sort"
2021-06-06 23:48:43 +00:00
"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
2021-11-06 20:25:31 +00:00
PersonalProjects [ ] templates . Project
2021-06-06 23:48:43 +00:00
ProjectAtomFeedUrl string
2021-09-05 19:57:10 +00:00
WIPForumUrl string
2021-06-06 23:48:43 +00:00
}
func ProjectIndex ( c * RequestContext ) ResponseData {
const projectsPerPage = 20
const maxCarouselProjects = 10
2021-11-06 20:25:31 +00:00
const maxPersonalProjects = 10
2021-06-06 23:48:43 +00:00
2021-11-06 20:25:31 +00:00
officialProjects , err := FetchProjects ( c . Context ( ) , c . Conn , c . CurrentUser , ProjectsQuery {
Types : OfficialProjects ,
} )
2021-06-06 23:48:43 +00:00
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch projects" ) )
2021-06-06 23:48:43 +00:00
}
2021-11-06 20:25:31 +00:00
numPages := int ( math . Ceil ( float64 ( len ( officialProjects ) ) / projectsPerPage ) )
page , numPages , ok := getPageInfo ( c . PathParams [ "page" ] , len ( officialProjects ) , feedPostsPerPage )
if ! ok {
return c . Redirect ( hmnurl . BuildProjectIndex ( 1 ) , http . StatusSeeOther )
2021-06-06 23:48:43 +00:00
}
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 ) ) ,
}
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 ( )
2021-11-06 20:25:31 +00:00
for _ , p := range officialProjects {
2021-11-10 04:11:39 +00:00
templateProject := templates . ProjectToTemplate ( & p . Project , UrlContextForProject ( & p . Project ) . BuildHomepage ( ) , c . Theme )
2021-11-06 20:25:31 +00:00
if p . Project . Slug == "hero" {
2021-06-06 23:48:43 +00:00
// NOTE(asaf): Handmade Hero gets special treatment. Must always be first in the list.
handmadeHero = & templateProject
continue
}
2021-11-06 20:25:31 +00:00
if p . Project . Featured {
2021-06-06 23:48:43 +00:00
featuredProjects = append ( featuredProjects , templateProject )
2021-11-06 20:25:31 +00:00
} else if now . Sub ( p . Project . AllLastUpdated ) . Seconds ( ) < models . RecentProjectUpdateTimespanSec {
2021-06-06 23:48:43 +00:00
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 ( )
2021-11-06 20:25:31 +00:00
// Fetch and highlight a random selection of personal projects
var personalProjects [ ] templates . Project
{
projects , err := FetchProjects ( c . Context ( ) , c . Conn , c . CurrentUser , ProjectsQuery {
Types : PersonalProjects ,
} )
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch personal projects" ) )
}
2021-11-12 00:35:00 +00:00
sort . Slice ( projects , func ( i , j int ) bool {
p1 := projects [ i ] . Project
p2 := projects [ j ] . Project
return p2 . AllLastUpdated . Before ( p1 . AllLastUpdated ) // sort backwards - recent first
} )
2021-11-06 20:25:31 +00:00
for i , p := range projects {
if i >= maxPersonalProjects {
break
}
2021-11-10 04:11:39 +00:00
personalProjects = append ( personalProjects , templates . ProjectToTemplate (
& p . Project ,
UrlContextForProject ( & p . Project ) . BuildHomepage ( ) ,
c . Theme ,
) )
2021-11-06 20:25:31 +00:00
}
}
2021-09-09 02:51:43 +00:00
baseData := getBaseDataAutocrumb ( c , "Projects" )
2021-06-06 23:48:43 +00:00
var res ResponseData
2021-07-17 15:19:17 +00:00
res . MustWriteTemplate ( "project_index.html" , ProjectTemplateData {
2021-06-06 23:48:43 +00:00
BaseData : baseData ,
Pagination : pagination ,
CarouselProjects : carouselProjects ,
Projects : pageProjects ,
2021-11-06 20:25:31 +00:00
PersonalProjects : personalProjects ,
2021-06-06 23:48:43 +00:00
ProjectAtomFeedUrl : hmnurl . BuildAtomFeedForProjects ( ) ,
2021-11-10 04:11:39 +00:00
WIPForumUrl : hmnurl . HMNProjectContext . BuildForum ( [ ] string { "wip" } , 1 ) ,
2021-06-06 23:48:43 +00:00
} , c . Perf )
return res
}
2021-07-08 07:40:30 +00:00
type ProjectHomepageData struct {
templates . BaseData
Project templates . Project
Owners [ ] templates . User
Screenshots [ ] string
ProjectLinks [ ] templates . Link
Licenses [ ] templates . Link
RecentActivity [ ] templates . TimelineItem
}
func ProjectHomepage ( c * RequestContext ) ResponseData {
maxRecentActivity := 15
2021-11-09 19:14:38 +00:00
if c . CurrentProject == nil {
2021-07-08 07:40:30 +00:00
return FourOhFour ( c )
}
2021-11-08 19:16:54 +00:00
// There are no further permission checks to do, because permissions are
// checked whatever way we fetch the project.
2021-11-09 19:14:38 +00:00
owners , err := FetchProjectOwners ( c . Context ( ) , c . Conn , c . CurrentProject . ID )
2021-07-08 07:40:30 +00:00
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , err )
2021-07-08 07:40:30 +00:00
}
c . Perf . StartBlock ( "SQL" , "Fetching screenshots" )
type screenshotQuery struct {
Filename string ` db:"screenshot.file" `
}
screenshotQueryResult , err := db . Query ( c . Context ( ) , c . Conn , screenshotQuery { } ,
`
SELECT $ columns
FROM
handmade_imagefile AS screenshot
INNER JOIN handmade_project_screenshots ON screenshot . id = handmade_project_screenshots . imagefile_id
WHERE
handmade_project_screenshots . project_id = $ 1
` ,
2021-11-09 19:14:38 +00:00
c . CurrentProject . ID ,
2021-07-08 07:40:30 +00:00
)
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch screenshots for project" ) )
2021-07-08 07:40:30 +00:00
}
c . Perf . EndBlock ( )
c . Perf . StartBlock ( "SQL" , "Fetching project links" )
type projectLinkQuery struct {
Link models . Link ` db:"link" `
}
projectLinkResult , err := db . Query ( c . Context ( ) , c . Conn , projectLinkQuery { } ,
`
SELECT $ columns
FROM
handmade_links as link
WHERE
link . project_id = $ 1
ORDER BY link . ordering ASC
` ,
2021-11-09 19:14:38 +00:00
c . CurrentProject . ID ,
2021-07-08 07:40:30 +00:00
)
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch project links" ) )
2021-07-08 07:40:30 +00:00
}
c . Perf . EndBlock ( )
2021-07-30 03:40:47 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch subforum tree" )
subforumTree := models . GetFullSubforumTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeSubforumLineageBuilder ( subforumTree )
2021-07-08 07:40:30 +00:00
c . Perf . EndBlock ( )
c . Perf . StartBlock ( "SQL" , "Fetching project timeline" )
type postQuery struct {
Post models . Post ` db:"post" `
Thread models . Thread ` db:"thread" `
Author models . User ` db:"author" `
}
postQueryResult , err := db . Query ( c . Context ( ) , c . Conn , postQuery { } ,
`
SELECT $ columns
FROM
handmade_post AS post
INNER JOIN handmade_thread AS thread ON thread . id = post . thread_id
INNER JOIN auth_user AS author ON author . id = post . author_id
WHERE
post . project_id = $ 1
ORDER BY post . postdate DESC
LIMIT $ 2
` ,
2021-11-09 19:14:38 +00:00
c . CurrentProject . ID ,
2021-07-08 07:40:30 +00:00
maxRecentActivity ,
)
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch project posts" ) )
2021-07-08 07:40:30 +00:00
}
c . Perf . EndBlock ( )
2021-11-11 20:00:36 +00:00
var templateData ProjectHomepageData
2021-07-08 07:40:30 +00:00
2021-11-11 20:00:36 +00:00
templateData . BaseData = getBaseData ( c , c . CurrentProject . Name , nil )
2021-11-09 19:14:38 +00:00
//if canEdit {
// // TODO: Move to project-specific navigation
2021-11-11 20:00:36 +00:00
// // templateData.BaseData.Header.EditURL = hmnurl.BuildProjectEdit(project.Slug, "")
2021-11-09 19:14:38 +00:00
//}
2021-11-11 20:00:36 +00:00
templateData . BaseData . OpenGraphItems = append ( templateData . BaseData . OpenGraphItems , templates . OpenGraphItem {
2021-09-09 02:51:43 +00:00
Property : "og:description" ,
2021-11-09 19:14:38 +00:00
Value : c . CurrentProject . Blurb ,
2021-09-09 02:51:43 +00:00
} )
2021-11-11 20:00:36 +00:00
templateData . Project = templates . ProjectToTemplate ( c . CurrentProject , c . UrlContext . BuildHomepage ( ) , c . Theme )
2021-07-23 03:09:46 +00:00
for _ , owner := range owners {
2021-11-11 20:00:36 +00:00
templateData . Owners = append ( templateData . Owners , templates . UserToTemplate ( owner , c . Theme ) )
2021-07-08 07:40:30 +00:00
}
2021-11-09 19:14:38 +00:00
if c . CurrentProject . Hidden {
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"hidden" ,
"NOTICE: This project is hidden. It is currently visible only to owners and site admins." ,
)
2021-07-08 07:40:30 +00:00
}
2021-11-09 19:14:38 +00:00
if c . CurrentProject . Lifecycle != models . ProjectLifecycleActive {
switch c . CurrentProject . Lifecycle {
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleUnapproved :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"unapproved" ,
fmt . Sprintf (
"NOTICE: This project has not yet been submitted for approval. It is only visible to owners. Please <a href=\"%s\">submit it for approval</a> when the project content is ready for review." ,
2021-11-10 04:11:39 +00:00
c . UrlContext . BuildProjectEdit ( "submit" ) ,
2021-08-17 05:18:04 +00:00
) ,
)
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleApprovalRequired :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"unapproved" ,
"NOTICE: This project is awaiting approval. It is only visible to owners and site admins." ,
)
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleHiatus :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"hiatus" ,
"NOTICE: This project is on hiatus and may not update for a while." ,
)
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleDead :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"dead" ,
"NOTICE: Site staff have marked this project as being dead. If you intend to revive it, please contact a member of the Handmade Network staff." ,
)
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleLTSRequired :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"lts-reqd" ,
"NOTICE: This project is awaiting approval for maintenance-mode status." ,
)
2021-07-08 07:40:30 +00:00
case models . ProjectLifecycleLTS :
2021-11-11 20:00:36 +00:00
templateData . BaseData . AddImmediateNotice (
2021-08-17 05:18:04 +00:00
"lts" ,
"NOTICE: This project has reached a state of completion." ,
)
2021-07-08 07:40:30 +00:00
}
}
for _ , screenshot := range screenshotQueryResult . ToSlice ( ) {
2021-11-11 20:00:36 +00:00
templateData . Screenshots = append ( templateData . Screenshots , hmnurl . BuildUserFile ( screenshot . ( * screenshotQuery ) . Filename ) )
2021-07-08 07:40:30 +00:00
}
for _ , link := range projectLinkResult . ToSlice ( ) {
2021-11-11 20:00:36 +00:00
templateData . ProjectLinks = append ( templateData . ProjectLinks , templates . LinkToTemplate ( & link . ( * projectLinkQuery ) . Link ) )
2021-07-08 07:40:30 +00:00
}
for _ , post := range postQueryResult . ToSlice ( ) {
2021-11-11 20:00:36 +00:00
templateData . RecentActivity = append ( templateData . RecentActivity , PostToTimelineItem (
2021-11-10 04:11:39 +00:00
c . UrlContext ,
2021-07-08 07:40:30 +00:00
lineageBuilder ,
& post . ( * postQuery ) . Post ,
& post . ( * postQuery ) . Thread ,
& post . ( * postQuery ) . Author ,
c . Theme ,
) )
}
2021-11-11 20:00:36 +00:00
tagId := - 1
if c . CurrentProject . TagID != nil {
tagId = * c . CurrentProject . TagID
}
snippets , err := FetchSnippets ( c . Context ( ) , c . Conn , c . CurrentUser , SnippetQuery {
Tags : [ ] int { tagId } ,
} )
if err != nil {
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch project snippets" ) )
}
for _ , s := range snippets {
item := SnippetToTimelineItem (
& s . Snippet ,
s . Asset ,
s . DiscordMessage ,
s . Tags ,
s . Owner ,
c . Theme ,
)
item . SmallInfo = true
templateData . RecentActivity = append ( templateData . RecentActivity , item )
}
c . Perf . StartBlock ( "PROFILE" , "Sort timeline" )
sort . Slice ( templateData . RecentActivity , func ( i , j int ) bool {
return templateData . RecentActivity [ j ] . Date . Before ( templateData . RecentActivity [ i ] . Date )
} )
c . Perf . EndBlock ( )
2021-07-08 07:40:30 +00:00
var res ResponseData
2021-11-11 20:00:36 +00:00
err = res . WriteTemplate ( "project_homepage.html" , templateData , c . Perf )
2021-07-08 07:40:30 +00:00
if err != nil {
2021-08-28 12:21:03 +00:00
return c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to render project homepage template" ) )
2021-07-08 07:40:30 +00:00
}
return res
}