Add projects / following UI to home page

This commit is contained in:
Ben Visness 2024-06-21 20:13:20 -05:00
parent 7144db58ed
commit 86825f1c09
23 changed files with 374 additions and 188 deletions

View File

@ -7189,8 +7189,10 @@ code {
--notice-warn-color: #aa7d30;
--notice-failure-color: #b42222;
--spoiler-border: #aaa;
--site-width: 80rem;
--site-width-narrow: 60rem;
--site-width: 54rem;
--site-width-narrow: 40rem;
--avatar-size-small: 1.5rem;
--avatar-size-normal: 2.5rem;
}
@media (prefers-color-scheme: dark) {
:root {
@ -7756,7 +7758,6 @@ pre,
}
.svgicon svg {
fill: currentColor;
stroke: currentColor;
width: 1em;
height: 1em;
overflow: visible;
@ -7764,6 +7765,10 @@ pre,
.svgicon:not(.svgicon-nofix) svg {
transform: translate(0px, 0.1em);
}
.svgicon-lite svg {
fill: currentColor;
overflow: visible;
}
.sr {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
@ -8467,6 +8472,8 @@ header.old .submenu > a {
}
header {
background-color: var(--bg-3);
border-bottom-style: solid;
border-bottom-width: 1px;
}
header .hmn-logo {
font-family: "MohaveHMN", sans-serif;
@ -8478,10 +8485,10 @@ header .hmn-logo {
header .menu-chevron {
display: inline-block;
margin-left: var(--spacing-extra-small);
font-size: var(--font-size-7);
}
header .avatar {
width: 1.8rem;
height: 1.8rem;
}
header .header-nav > a,
header .header-nav > .root-item > a {
@ -8495,6 +8502,9 @@ header .header-nav .submenu {
z-index: 100;
min-width: 8rem;
background-color: var(--card-background);
border-style: solid;
border-width: 1px;
border-top-width: 0;
}
header .header-nav .submenu > a {
padding: var(--spacing-small) var(--spacing-medium);
@ -8880,10 +8890,19 @@ code .ss,
/* src/rawdata/scss/timeline.css */
.avatar {
object-fit: cover;
border-radius: 100%;
overflow: hidden;
background-color: var(--dimmest-color);
flex-shrink: 0;
border: none;
width: var(--avatar-size-normal);
height: var(--avatar-size-normal);
}
.avatar.avatar-user {
border-radius: 999px;
}
.avatar.avatar-small {
width: var(--avatar-size-small);
height: var(--avatar-size-small);
}
.timeline-item {
background-color: var(--card-background);

View File

@ -44,6 +44,8 @@ func FetchSnippets(
}
defer tx.Rollback(ctx)
isFiltering := len(q.IDs) > 0 || len(q.Tags) > 0 || len(q.ProjectIDs) > 0
var tagSnippetIDs []int
if len(q.Tags) > 0 {
// Get snippet IDs with this tag, then use that in the main query
@ -103,15 +105,17 @@ func FetchSnippets(
allSnippetIDs = append(allSnippetIDs, q.IDs...)
allSnippetIDs = append(allSnippetIDs, tagSnippetIDs...)
allSnippetIDs = append(allSnippetIDs, projectSnippetIDs...)
if len(allSnippetIDs) == 0 {
if isFiltering && len(allSnippetIDs) == 0 {
// We already managed to filter out all snippets, and all further
// parts of this query are more filters, so we can just fail everything
// else from right here.
qb.Add(`AND FALSE`)
} else if len(q.OwnerIDs) > 0 {
} else if len(allSnippetIDs) > 0 && len(q.OwnerIDs) > 0 {
qb.Add(`AND (snippet.id = ANY ($?) OR snippet.owner_id = ANY ($?))`, allSnippetIDs, q.OwnerIDs)
} else {
if len(allSnippetIDs) > 0 {
qb.Add(`AND snippet.id = ANY ($?)`, allSnippetIDs)
}
if len(q.OwnerIDs) > 0 {
qb.Add(`AND snippet.owner_id = ANY ($?)`, q.OwnerIDs)
}

7
src/models/follow.go Normal file
View File

@ -0,0 +1,7 @@
package models
type Follow struct {
UserID int `db:"user_id"`
FollowingUserID *int `db:"following_user_id"`
FollowingProjectID *int `db:"following_project_id"`
}

View File

@ -683,10 +683,14 @@ pre,
}
}
/*
TODO(redesign): It's really unfortunate that we rely on text stuff so much...it
makes all our SVGs fuzzy. Evaluate the places we use this and see if we can use the
lite variant instead.
*/
.svgicon {
svg {
fill: currentColor;
stroke: currentColor;
width: 1em;
height: 1em;
overflow: visible;
@ -697,6 +701,13 @@ pre,
}
}
.svgicon-lite {
svg {
fill: currentColor;
overflow: visible;
}
}
.sr {
border: 0;
clip: rect(1px, 1px, 1px, 1px);

View File

@ -115,6 +115,8 @@ header.old {
header {
background-color: var(--bg-3);
border-bottom-style: solid;
border-bottom-width: 1px;
.hmn-logo {
font-family: 'MohaveHMN', sans-serif;
@ -128,11 +130,11 @@ header {
/* ensure that it also has .svgicon */
display: inline-block;
margin-left: var(--spacing-extra-small);
font-size: var(--font-size-7);
}
.avatar {
width: 1.8rem;
height: 1.8rem;
}
.header-nav {
@ -150,6 +152,9 @@ header {
z-index: 100;
min-width: 8rem;
background-color: var(--card-background);
border-style: solid;
border-width: 1px;
border-top-width: 0;
>a {
padding: var(--spacing-small) var(--spacing-medium);

View File

@ -1,9 +1,21 @@
.avatar {
object-fit: cover;
border-radius: 100%;
overflow: hidden;
background-color: var(--dimmest-color);
flex-shrink: 0;
border: none;
width: var(--avatar-size-normal);
height: var(--avatar-size-normal);
}
.avatar.avatar-user {
border-radius: 999px;
}
.avatar.avatar-small {
width: var(--avatar-size-small);
height: var(--avatar-size-small);
}
.timeline-item {

View File

@ -56,8 +56,11 @@ $breakpoint-large: screen and (min-width: 60em)
--spoiler-border: #aaa;
--site-width: 80rem;
--site-width-narrow: 60rem;
--site-width: 54rem;
--site-width-narrow: 40rem;
--avatar-size-small: 1.5rem;
--avatar-size-normal: 2.5rem;
}
@media (prefers-color-scheme: dark) {

View File

@ -1 +1,3 @@
<svg viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M6,6l0,-6l2,0l0,6l6,0l0,2l-6,0l0,6l-2,0l0,-6l-6,0l0,-2l6,0Z"/></svg>
<svg width="14" height="14" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M6,6L6,0L8,0L8,6L14,6L14,8L8,8L8,14L6,14L6,8L0,8L0,6L6,6Z" style="fill-rule:nonzero;"/>
</svg>

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 393 B

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 186 186" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(6.12323e-17,1,-1,6.12323e-17,185.342,0.00025)">
<svg width="10" height="10" viewBox="0 0 10 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(3.30372e-18,0.0539539,-0.0539539,3.30372e-18,9.99997,4.04654e-05)">
<path d="M51.707,185.343C48.966,185.343 46.214,184.299 44.114,182.194C39.92,178 39.92,171.213 44.114,167.019L118.466,92.672L44.114,18.32C39.92,14.126 39.92,7.333 44.114,3.145C48.308,-1.049 55.101,-1.049 59.294,3.145L141.228,85.079C145.422,89.273 145.422,96.066 141.228,100.254L59.294,182.193C57.201,184.293 54.454,185.343 51.707,185.343Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 910 B

After

Width:  |  Height:  |  Size: 770 B

View File

@ -74,7 +74,6 @@ func ProjectLogoUrl(p *models.Project, lightAsset *models.Asset, darkAsset *mode
func ProjectToTemplate(
p *models.Project,
url string,
) Project {
return Project{
ID: p.ID,
@ -82,7 +81,7 @@ func ProjectToTemplate(
Subdomain: p.Subdomain(),
Color1: p.Color1,
Color2: p.Color2,
Url: url,
Url: hmndata.UrlContextForProject(p).BuildHomepage(),
Blurb: p.Blurb,
ParsedDescription: template.HTML(p.ParsedDescription),
@ -98,8 +97,8 @@ func ProjectToTemplate(
}
}
func ProjectAndStuffToTemplate(p *hmndata.ProjectAndStuff, url string) Project {
res := ProjectToTemplate(&p.Project, url)
func ProjectAndStuffToTemplate(p *hmndata.ProjectAndStuff) Project {
res := ProjectToTemplate(&p.Project)
res.Logo = ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset)
for _, o := range p.Owners {
res.Owners = append(res.Owners, UserToTemplate(o))

View File

@ -1,4 +1,4 @@
<header id="site-header" class="bb flex flex-row items-center link--normal">
<header id="site-header" class="flex flex-row items-center link--normal">
<a href="{{ .Header.HMNHomepageUrl }}" class="hmn-logo flex-shrink-0">
Handmade
</a>
@ -8,7 +8,7 @@
<a href="{{ .Header.JamsUrl }}">Jams</a>
<div class="root-item">
<a aria-expanded="false" aria-controls="events-submenu" class="menu-dropdown-js" href="#">
Events <div class="menu-chevron svgicon">{{ svg "chevron-down-thick" }}</div>
Events <div class="menu-chevron svgicon-lite">{{ svg "chevron-down" }}</div>
</a>
<div class="submenu" id="events-submenu">
<a href="{{ .Header.JamsUrl }}">Jams</a>
@ -17,7 +17,7 @@
</div>
<div class="root-item">
<a aria-expanded="false" aria-controls="resource-submenu" class="menu-dropdown-js" href="#">
Resources <div class="menu-chevron svgicon">{{ svg "chevron-down-thick" }}</div>
Resources <div class="menu-chevron svgicon-lite">{{ svg "chevron-down" }}</div>
</a>
<div class="submenu" id="resource-submenu">
<a href="{{ .Header.ForumsUrl }}">Forums</a>
@ -31,7 +31,7 @@
</div>
<div class="root-item">
<a aria-expanded="false" aria-controls="about-submenu" class="menu-dropdown-js" href="#">
About <div class="menu-chevron svgicon">{{ svg "chevron-down-thick" }}</div>
About <div class="menu-chevron svgicon-lite">{{ svg "chevron-down" }}</div>
</a>
<div class="submenu" id="about-submenu">
<a href="{{ .Header.ManifestoUrl }}">Manifesto</a>
@ -40,7 +40,7 @@
</div>
</div>
<a class="db ph3 pv2 flex" href="{{ or .Header.UserProfileUrl .LoginPageUrl }}">
<img class="avatar" src="{{ with .User }}{{ .AvatarUrl }}{{ end }}">
<img class="avatar avatar-user" src="{{ with .User }}{{ .AvatarUrl }}{{ end }}">
</a>
</header>
<script type="text/javascript">

View File

@ -3,7 +3,7 @@
<div class="flex items-center">
{{ if .OwnerAvatarUrl }}
<a class="flex flex-shrink-0" href="{{ .OwnerUrl }}">
<img class="avatar {{ if not .SmallInfo }}big{{ end }} {{ if .SmallInfo }}mr2{{ else }}mr3{{ end }}" src="{{ .OwnerAvatarUrl }}" />
<img class="avatar avatar-user {{ if not .SmallInfo }}big{{ end }} {{ if .SmallInfo }}mr2{{ else }}mr3{{ end }}" src="{{ .OwnerAvatarUrl }}" />
</a>
{{ end }}

View File

@ -84,29 +84,43 @@
<div class="flex justify-center pa3">
<div class="w-100 mw-site flex g3">
<!-- Sidebar -->
<div class="w5 flex flex-column g2">
<div class="bg--card pa2">
<div class="pb2 flex justify-between items-center">
<div class="w5 flex flex-column g2 flex-shrink-0">
{{ if .User }}
<div class="bg--card link--normal">
<div class="pa2 flex justify-between items-center">
<span class="f7">Your projects</span>
<span class="svgicon f8">{{ svg "chevron-down" }}</span>
<span class="svgicon-lite">{{ svg "chevron-down" }}</span>
</div>
<div>
You have not created any projects.
<div class="ph2 flex flex-column g2">
{{ range .UserProjects }}
{{ template "list-project" . }}
{{ else }}
<div class="f7 pv3 tc c--dim">You have not created any projects.</div>
{{ end }}
</div>
<a class="bt mt2 pa2 flex justify-between" href="{{ .NewProjectUrl }}">
<div>Create new project</div>
<div class="svgicon-lite flex items-center">{{ svg "add" }}</div>
</a>
</div>
<div class="bg--card pa2">
<div class="pb2 flex justify-between items-center">
<div class="bg--card link--normal">
<div class="pa2 flex justify-between items-center">
<span class="f7">Following</span>
<span class="svgicon f8">{{ svg "chevron-down" }}</span>
</div>
<div>
You are not following anything yet.
<div class="ph2 pb2 flex flex-column g2">
{{ range .Following }}
{{ template "list-follow" . }}
{{ else }}
<div class="f7 pv3 tc c--dim">You are not following anything.</div>
{{ end }}
</div>
</div>
{{ end }}
</div>
<!-- Feed -->
<div class="flex flex-column flex-grow-1">
<div class="flex flex-column flex-grow-1 overflow-hidden">
<div class="timeline flex flex-column g3">
{{ range .RecentItems }}
{{ template "timeline_item.html" . }}
@ -118,3 +132,38 @@
</div>
{{ end }}
{{ define "list-project" }}
<a class="flex g2 items-center" href="{{ .Url }}">
{{ with .Logo }}
<img class="avatar avatar-small" src="{{ . }}">
{{ else }}
<div class="avatar avatar-small"></div>
{{ end }}
<div>{{ .Name }}</div>
</a>
{{ end }}
{{ define "list-follow" }}
{{ if .User }}
<a class="flex g2 items-center" href="{{ .User.ProfileUrl }}">
{{ with .User.AvatarUrl }}
<img class="avatar avatar-user avatar-small" src="{{ . }}">
{{ else }}
<div class="avatar avatar-user avatar-small"></div>
{{ end }}
<div>{{ .User.Name }}</div>
</a>
{{ else if .Project }}
<a class="flex g2 items-center" href="{{ .Project.Url }}">
{{ with .Project.Logo }}
<img class="avatar avatar-small" src="{{ . }}">
{{ else }}
<div class="avatar avatar-small"></div>
{{ end }}
<div>{{ .Project.Name }}</div>
</a>
{{ else }}
???
{{ end }}
{{ end }}

View File

@ -177,6 +177,11 @@ type Asset struct {
Width, Height int
}
type Follow struct {
User *User
Project *Project
}
type ProjectJamParticipation struct {
JamName string
JamSlug string

View File

@ -236,7 +236,7 @@ func AdminApprovalQueue(c *RequestContext) ResponseData {
userData.Date = p.ProjectAndStuff.Project.DateCreated
}
userData.ProjectsWithLinks = append(userData.ProjectsWithLinks, projectWithLinks{
Project: templates.ProjectAndStuffToTemplate(p.ProjectAndStuff, hmndata.UrlContextForProject(&p.ProjectAndStuff.Project).BuildHomepage()),
Project: templates.ProjectAndStuffToTemplate(p.ProjectAndStuff),
Links: projectLinks,
})
}

View File

@ -54,7 +54,7 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
DiscordInviteUrl: "https://discord.gg/hmn",
NewsletterSignupUrl: hmnurl.BuildAPINewsletterSignup(),
Project: templates.ProjectToTemplate(&project, c.UrlContext.BuildHomepage()),
Project: templates.ProjectToTemplate(&project),
User: templateUser,
Session: templateSession,
Notices: notices,

View File

@ -165,7 +165,7 @@ func AtomFeed(c *RequestContext) ResponseData {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects"))
}
for _, p := range projectsAndStuff {
templateProject := templates.ProjectToTemplate(&p.Project, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
templateProject := templates.ProjectToTemplate(&p.Project)
templateProject.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(templateProject.Url)).URN()
for _, owner := range p.Owners {
templateProject.Owners = append(templateProject.Owners, templates.UserToTemplate(owner))

View File

@ -250,9 +250,7 @@ func getLJ2024FeedData(c *RequestContext, maxTimelineItems int) (JamFeedDataLJ20
projects := make([]templates.Project, 0, len(jamProjects))
for _, jp := range jamProjects {
urlContext := hmndata.UrlContextForProject(&jp.Project)
projectUrl := urlContext.BuildHomepage()
projects = append(projects, templates.ProjectAndStuffToTemplate(&jp, projectUrl))
projects = append(projects, templates.ProjectAndStuffToTemplate(&jp))
}
projectIds := make([]int, 0, len(jamProjects))
@ -367,7 +365,7 @@ func JamIndex2023(c *RequestContext) ResponseData {
pageProjects := make([]templates.Project, 0, len(jamProjects))
for _, p := range jamProjects {
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage()))
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p))
}
projectIds := make([]int, 0, len(jamProjects))
@ -460,7 +458,7 @@ func JamFeed2023(c *RequestContext) ResponseData {
pageProjects := make([]templates.Project, 0, len(jamProjects))
for _, p := range jamProjects {
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage()))
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p))
}
type JamFeedData struct {
@ -553,7 +551,7 @@ func JamIndex2023_Visibility(c *RequestContext) ResponseData {
pageProjects := make([]templates.Project, 0, len(jamProjects))
for _, p := range jamProjects {
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage()))
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p))
}
projectIds := make([]int, 0, len(jamProjects))
@ -628,7 +626,7 @@ func JamFeed2023_Visibility(c *RequestContext) ResponseData {
pageProjects := make([]templates.Project, 0, len(jamProjects))
for _, p := range jamProjects {
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage()))
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p))
}
type JamFeedData struct {
@ -767,7 +765,7 @@ func JamIndex2022(c *RequestContext) ResponseData {
pageProjects := make([]templates.Project, 0, len(jamProjects))
for _, p := range jamProjects {
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage()))
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p))
}
projectIds := make([]int, 0, len(jamProjects))
@ -841,7 +839,7 @@ func JamFeed2022(c *RequestContext) ResponseData {
pageProjects := make([]templates.Project, 0, len(jamProjects))
for _, p := range jamProjects {
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage()))
pageProjects = append(pageProjects, templates.ProjectAndStuffToTemplate(&p))
}
type JamFeedData struct {

View File

@ -17,6 +17,31 @@ func Index(c *RequestContext) ResponseData {
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
c.Perf.EndBlock()
type LandingTemplateData struct {
templates.BaseData
NewsPost *templates.TimelineItem
FollowingItems []templates.TimelineItem
FeaturedItems []templates.TimelineItem
RecentItems []templates.TimelineItem
NewsItems []templates.TimelineItem
UserProjects []templates.Project
Following []templates.Follow
ManifestoUrl string
PodcastUrl string
AtomFeedUrl string
MarkAllReadUrl string
NewProjectUrl string
JamUrl string
JamDaysUntilStart, JamDaysUntilEnd int
HMSDaysUntilStart, HMSDaysUntilEnd int
HMBostonDaysUntilStart, HMBostonDaysUntilEnd int
}
var err error
var followingItems []templates.TimelineItem
var featuredItems []templates.TimelineItem
@ -59,25 +84,26 @@ func Index(c *RequestContext) ResponseData {
}
c.Perf.EndBlock()
type LandingTemplateData struct {
templates.BaseData
var projects []templates.Project
if c.CurrentUser != nil {
projectsDb, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
OwnerIDs: []int{c.CurrentUser.ID},
})
if err != nil {
c.Logger.Warn().Err(err).Msg("failed to fetch user projects")
}
NewsPost *templates.TimelineItem
FollowingItems []templates.TimelineItem
FeaturedItems []templates.TimelineItem
RecentItems []templates.TimelineItem
NewsItems []templates.TimelineItem
for _, p := range projectsDb {
projects = append(projects, templates.ProjectAndStuffToTemplate(&p))
}
}
ManifestoUrl string
PodcastUrl string
AtomFeedUrl string
MarkAllReadUrl string
JamUrl string
JamDaysUntilStart, JamDaysUntilEnd int
HMSDaysUntilStart, HMSDaysUntilEnd int
HMBostonDaysUntilStart, HMBostonDaysUntilEnd int
var follows []templates.Follow
if c.CurrentUser != nil {
follows, err = FetchFollows(c, c.Conn, c.CurrentUser, c.CurrentUser.ID)
if err != nil {
c.Logger.Warn().Err(err).Msg("failed to fetch user follows")
}
}
baseData := getBaseData(c, "", nil)
@ -96,10 +122,14 @@ func Index(c *RequestContext) ResponseData {
RecentItems: recentItems,
NewsItems: newsItems,
UserProjects: projects,
Following: follows,
ManifestoUrl: hmnurl.BuildManifesto(),
PodcastUrl: hmnurl.BuildPodcast(),
AtomFeedUrl: hmnurl.BuildAtomFeed(),
MarkAllReadUrl: hmnurl.HMNProjectContext.BuildForumMarkRead(0),
NewProjectUrl: hmnurl.BuildProjectNew(),
JamUrl: hmnurl.BuildJamIndex2024_Learning(),
JamDaysUntilStart: daysUntil(hmndata.LJ2024.StartTime),

View File

@ -238,7 +238,7 @@ func getShuffledOfficialProjects(c *RequestContext) ([]templates.Project, error)
var restProjects []templates.Project
now := time.Now()
for _, p := range official {
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
templateProject := templates.ProjectAndStuffToTemplate(&p)
if p.Project.Slug == "hero" {
// NOTE(asaf): Handmade Hero gets special treatment. Must always be first in the list.
@ -297,7 +297,7 @@ func getPersonalProjects(c *RequestContext, jamSlug string) ([]templates.Project
var personalProjects []templates.Project
for _, p := range projects {
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
templateProject := templates.ProjectAndStuffToTemplate(&p)
personalProjects = append(personalProjects, templateProject)
}
@ -410,7 +410,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project details"))
}
templateData.Project = templates.ProjectAndStuffToTemplate(&p, c.UrlContext.BuildHomepage())
templateData.Project = templates.ProjectAndStuffToTemplate(&p)
for _, owner := range owners {
templateData.Owners = append(templateData.Owners, templates.UserToTemplate(owner))
}
@ -486,12 +486,12 @@ func ProjectHomepage(c *RequestContext) ResponseData {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user projects"))
}
templateProjects := make([]templates.Project, 0, len(userProjects))
templateProjects = append(templateProjects, templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage()))
templateProjects = append(templateProjects, templates.ProjectAndStuffToTemplate(&p))
for _, p := range userProjects {
if p.Project.ID == c.CurrentProject.ID {
continue
}
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
templateProject := templates.ProjectAndStuffToTemplate(&p)
templateProjects = append(templateProjects, templateProject)
}
templateData.SnippetEdit = templates.SnippetEdit{

View File

@ -114,7 +114,7 @@ func Snippet(c *RequestContext) ResponseData {
}
templateProjects := make([]templates.Project, 0, len(userProjects))
for _, p := range userProjects {
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
templateProject := templates.ProjectAndStuffToTemplate(&p)
templateProjects = append(templateProjects, templateProject)
}
snippetEdit = templates.SnippetEdit{

View File

@ -18,18 +18,14 @@ import (
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/perf"
"git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils"
)
func FetchFollowTimelineForUser(ctx context.Context, conn db.ConnOrTx, user *models.User) ([]templates.TimelineItem, error) {
perf := perf.ExtractPerf(ctx)
type Follower struct {
UserID int `db:"user_id"`
FollowingUserID *int `db:"following_user_id"`
FollowingProjectID *int `db:"following_project_id"`
}
perf.StartBlock("FOLLOW", "Assemble follow data")
following, err := db.Query[Follower](ctx, conn, `
following, err := db.Query[models.Follow](ctx, conn, `
SELECT $columns
FROM follower
WHERE user_id = $1
@ -68,86 +64,58 @@ type TimelineQuery struct {
func FetchTimeline(ctx context.Context, conn db.ConnOrTx, currentUser *models.User, q TimelineQuery) ([]templates.TimelineItem, error) {
perf := perf.ExtractPerf(ctx)
var users []*models.User
var projects []hmndata.ProjectAndStuff
// var users []*models.User
// var projects []hmndata.ProjectAndStuff
var snippets []hmndata.SnippetAndStuff
var posts []hmndata.PostAndStuff
var streamers []hmndata.TwitchStreamer
var streams []*models.TwitchStreamHistory
perf.StartBlock("TIMELINE", "Fetch timeline data")
if len(q.UserIDs) > 0 || len(q.ProjectIDs) > 0 {
// var streamers []hmndata.TwitchStreamer
// var streams []*models.TwitchStreamHistory
var err error
if len(q.UserIDs) > 0 {
users, err = hmndata.FetchUsers(ctx, conn, currentUser, hmndata.UsersQuery{
UserIDs: q.UserIDs,
})
if err != nil {
return nil, oops.New(err, "failed to fetch users")
}
}
// NOTE(asaf): Clear out invalid users in case we banned someone after they got followed
validUserIDs := make([]int, 0, len(q.UserIDs))
for _, u := range users {
validUserIDs = append(validUserIDs, u.ID)
}
if len(q.ProjectIDs) > 0 {
projects, err = hmndata.FetchProjects(ctx, conn, currentUser, hmndata.ProjectsQuery{
ProjectIDs: q.ProjectIDs,
})
if err != nil {
return nil, oops.New(err, "failed to fetch projects")
}
}
// NOTE(asaf): The original projectIDs might contain hidden/abandoned projects,
// so we recreate it after the projects get filtered by FetchProjects.
validProjectIDs := make([]int, 0, len(q.ProjectIDs))
for _, p := range projects {
validProjectIDs = append(validProjectIDs, p.Project.ID)
}
perf.StartBlock("TIMELINE", "Fetch timeline data")
{
snippets, err = hmndata.FetchSnippets(ctx, conn, currentUser, hmndata.SnippetQuery{
OwnerIDs: validUserIDs,
ProjectIDs: validProjectIDs,
OwnerIDs: q.UserIDs,
ProjectIDs: q.ProjectIDs,
Limit: q.Limit,
})
if err != nil {
return nil, oops.New(err, "failed to fetch user snippets")
return nil, oops.New(err, "failed to fetch timeline snippets")
}
posts, err = hmndata.FetchPosts(ctx, conn, currentUser, hmndata.PostsQuery{
UserIDs: validUserIDs,
ProjectIDs: validProjectIDs,
UserIDs: q.UserIDs,
ProjectIDs: q.ProjectIDs,
SortDescending: true,
Limit: q.Limit,
})
if err != nil {
return nil, oops.New(err, "failed to fetch user posts")
return nil, oops.New(err, "failed to fetch timeline posts")
}
streamers, err = hmndata.FetchTwitchStreamers(ctx, conn, hmndata.TwitchStreamersQuery{
UserIDs: validUserIDs,
ProjectIDs: validProjectIDs,
})
if err != nil {
return nil, oops.New(err, "failed to fetch streamers")
}
// streamers, err = hmndata.FetchTwitchStreamers(ctx, conn, hmndata.TwitchStreamersQuery{
// UserIDs: validUserIDs,
// ProjectIDs: validProjectIDs,
// })
// if err != nil {
// return nil, oops.New(err, "failed to fetch streamers")
// }
twitchLogins := make([]string, 0, len(streamers))
for _, s := range streamers {
twitchLogins = append(twitchLogins, s.TwitchLogin)
}
streams, err = db.Query[models.TwitchStreamHistory](ctx, conn,
`
SELECT $columns FROM twitch_stream_history WHERE twitch_login = ANY ($1)
`,
twitchLogins,
)
if err != nil {
return nil, oops.New(err, "failed to fetch stream histories")
}
// twitchLogins := make([]string, 0, len(streamers))
// for _, s := range streamers {
// twitchLogins = append(twitchLogins, s.TwitchLogin)
// }
// streams, err = db.Query[models.TwitchStreamHistory](ctx, conn,
// `
// SELECT $columns FROM twitch_stream_history WHERE twitch_login = ANY ($1)
// `,
// twitchLogins,
// )
// if err != nil {
// return nil, oops.New(err, "failed to fetch stream histories")
// }
}
perf.EndBlock()
@ -184,38 +152,38 @@ func FetchTimeline(ctx context.Context, conn db.ConnOrTx, currentUser *models.Us
timelineItems = append(timelineItems, item)
}
for _, s := range streams {
ownerAvatarUrl := ""
ownerName := ""
ownerUrl := ""
// for _, s := range streams {
// ownerAvatarUrl := ""
// ownerName := ""
// ownerUrl := ""
for _, streamer := range streamers {
if streamer.TwitchLogin == s.TwitchLogin {
if streamer.UserID != nil {
for _, u := range users {
if u.ID == *streamer.UserID {
ownerAvatarUrl = templates.UserAvatarUrl(u)
ownerName = u.BestName()
ownerUrl = hmnurl.BuildUserProfile(u.Username)
break
}
}
} else if streamer.ProjectID != nil {
for _, p := range projects {
if p.Project.ID == *streamer.ProjectID {
ownerAvatarUrl = templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset)
ownerName = p.Project.Name
ownerUrl = hmndata.UrlContextForProject(&p.Project).BuildHomepage()
}
break
}
}
break
}
}
item := TwitchStreamToTimelineItem(s, ownerAvatarUrl, ownerName, ownerUrl)
timelineItems = append(timelineItems, item)
}
// for _, streamer := range streamers {
// if streamer.TwitchLogin == s.TwitchLogin {
// if streamer.UserID != nil {
// for _, u := range users {
// if u.ID == *streamer.UserID {
// ownerAvatarUrl = templates.UserAvatarUrl(u)
// ownerName = u.BestName()
// ownerUrl = hmnurl.BuildUserProfile(u.Username)
// break
// }
// }
// } else if streamer.ProjectID != nil {
// for _, p := range projects {
// if p.Project.ID == *streamer.ProjectID {
// ownerAvatarUrl = templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset)
// ownerName = p.Project.Name
// ownerUrl = hmndata.UrlContextForProject(&p.Project).BuildHomepage()
// }
// break
// }
// }
// break
// }
// }
// item := TwitchStreamToTimelineItem(s, ownerAvatarUrl, ownerName, ownerUrl)
// timelineItems = append(timelineItems, item)
// }
perf.StartBlock("TIMELINE", "Sort timeline")
sort.Slice(timelineItems, func(i, j int) bool {
@ -224,9 +192,85 @@ func FetchTimeline(ctx context.Context, conn db.ConnOrTx, currentUser *models.Us
perf.EndBlock()
perf.EndBlock()
if q.Limit > 0 {
timelineItems = utils.ClampSlice(timelineItems, q.Limit)
}
return timelineItems, nil
}
func FetchFollows(ctx context.Context, conn db.ConnOrTx, currentUser *models.User, userID int) ([]templates.Follow, error) {
perf := perf.ExtractPerf(ctx)
perf.StartBlock("SQL", "Fetch follows")
following, err := db.Query[models.Follow](ctx, conn, `
SELECT $columns
FROM follower
WHERE user_id = $1
`, userID)
if err != nil {
return nil, oops.New(err, "failed to fetch follows")
}
perf.EndBlock()
var userIDs, projectIDs []int
for _, follow := range following {
if follow.FollowingUserID != nil {
userIDs = append(userIDs, *follow.FollowingUserID)
}
if follow.FollowingProjectID != nil {
projectIDs = append(projectIDs, *follow.FollowingProjectID)
}
}
var users []*models.User
var projectsAndStuff []hmndata.ProjectAndStuff
if len(userIDs) > 0 {
users, err = hmndata.FetchUsers(ctx, conn, currentUser, hmndata.UsersQuery{
UserIDs: userIDs,
})
if err != nil {
return nil, oops.New(err, "failed to fetch users for follows")
}
}
if len(projectIDs) > 0 {
projectsAndStuff, err = hmndata.FetchProjects(ctx, conn, currentUser, hmndata.ProjectsQuery{
ProjectIDs: projectIDs,
})
if err != nil {
return nil, oops.New(err, "failed to fetch projects for follows")
}
}
var result []templates.Follow
for _, follow := range following {
if follow.FollowingUserID != nil {
for _, user := range users {
if user.ID == *follow.FollowingUserID {
u := templates.UserToTemplate(user)
result = append(result, templates.Follow{
User: &u,
})
break
}
}
}
if follow.FollowingProjectID != nil {
for _, p := range projectsAndStuff {
if p.Project.ID == *follow.FollowingProjectID {
proj := templates.ProjectAndStuffToTemplate(&p)
result = append(result, templates.Follow{
Project: &proj,
})
break
}
}
}
}
return result, nil
}
type TimelineTypeTitles struct {
TypeTitleFirst string
TypeTitleNotFirst string
@ -369,7 +413,7 @@ func SnippetToTimelineItem(
return projects[i].Project.Name < projects[j].Project.Name
})
for _, proj := range projects {
item.Projects = append(item.Projects, templates.ProjectAndStuffToTemplate(proj, hmndata.UrlContextForProject(&proj.Project).BuildHomepage()))
item.Projects = append(item.Projects, templates.ProjectAndStuffToTemplate(proj))
}
return item

View File

@ -106,7 +106,7 @@ func UserProfile(c *RequestContext) ResponseData {
templateProjects := make([]templates.Project, 0, len(projectsAndStuff))
numPersonalProjects := 0
for _, p := range projectsAndStuff {
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
templateProject := templates.ProjectAndStuffToTemplate(&p)
templateProjects = append(templateProjects, templateProject)
if p.Project.Personal {