Add projects / following UI to home page
This commit is contained in:
parent
7144db58ed
commit
86825f1c09
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
qb.Add(`AND snippet.id = ANY ($?)`, allSnippetIDs)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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))
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 }}
|
||||
|
||||
|
|
|
@ -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">
|
||||
<span class="f7">Your projects</span>
|
||||
<span class="svgicon f8">{{ svg "chevron-down" }}</span>
|
||||
<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-lite">{{ svg "chevron-down" }}</span>
|
||||
</div>
|
||||
<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>
|
||||
You have not created any projects.
|
||||
<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 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>
|
||||
</div>
|
||||
<div class="bg--card pa2">
|
||||
<div class="pb2 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>
|
||||
</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 }}
|
||||
|
|
|
@ -177,6 +177,11 @@ type Asset struct {
|
|||
Width, Height int
|
||||
}
|
||||
|
||||
type Follow struct {
|
||||
User *User
|
||||
Project *Project
|
||||
}
|
||||
|
||||
type ProjectJamParticipation struct {
|
||||
JamName string
|
||||
JamSlug string
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
// var streamers []hmndata.TwitchStreamer
|
||||
// var streams []*models.TwitchStreamHistory
|
||||
var err error
|
||||
|
||||
perf.StartBlock("TIMELINE", "Fetch timeline data")
|
||||
if len(q.UserIDs) > 0 || len(q.ProjectIDs) > 0 {
|
||||
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)
|
||||
}
|
||||
|
||||
{
|
||||
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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue