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-warn-color: #aa7d30;
|
||||||
--notice-failure-color: #b42222;
|
--notice-failure-color: #b42222;
|
||||||
--spoiler-border: #aaa;
|
--spoiler-border: #aaa;
|
||||||
--site-width: 80rem;
|
--site-width: 54rem;
|
||||||
--site-width-narrow: 60rem;
|
--site-width-narrow: 40rem;
|
||||||
|
--avatar-size-small: 1.5rem;
|
||||||
|
--avatar-size-normal: 2.5rem;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
|
@ -7756,7 +7758,6 @@ pre,
|
||||||
}
|
}
|
||||||
.svgicon svg {
|
.svgicon svg {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
stroke: currentColor;
|
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
@ -7764,6 +7765,10 @@ pre,
|
||||||
.svgicon:not(.svgicon-nofix) svg {
|
.svgicon:not(.svgicon-nofix) svg {
|
||||||
transform: translate(0px, 0.1em);
|
transform: translate(0px, 0.1em);
|
||||||
}
|
}
|
||||||
|
.svgicon-lite svg {
|
||||||
|
fill: currentColor;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
.sr {
|
.sr {
|
||||||
border: 0;
|
border: 0;
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
|
@ -8467,6 +8472,8 @@ header.old .submenu > a {
|
||||||
}
|
}
|
||||||
header {
|
header {
|
||||||
background-color: var(--bg-3);
|
background-color: var(--bg-3);
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
header .hmn-logo {
|
header .hmn-logo {
|
||||||
font-family: "MohaveHMN", sans-serif;
|
font-family: "MohaveHMN", sans-serif;
|
||||||
|
@ -8478,10 +8485,10 @@ header .hmn-logo {
|
||||||
header .menu-chevron {
|
header .menu-chevron {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: var(--spacing-extra-small);
|
margin-left: var(--spacing-extra-small);
|
||||||
font-size: var(--font-size-7);
|
|
||||||
}
|
}
|
||||||
header .avatar {
|
header .avatar {
|
||||||
width: 1.8rem;
|
width: 1.8rem;
|
||||||
|
height: 1.8rem;
|
||||||
}
|
}
|
||||||
header .header-nav > a,
|
header .header-nav > a,
|
||||||
header .header-nav > .root-item > a {
|
header .header-nav > .root-item > a {
|
||||||
|
@ -8495,6 +8502,9 @@ header .header-nav .submenu {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
min-width: 8rem;
|
min-width: 8rem;
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-top-width: 0;
|
||||||
}
|
}
|
||||||
header .header-nav .submenu > a {
|
header .header-nav .submenu > a {
|
||||||
padding: var(--spacing-small) var(--spacing-medium);
|
padding: var(--spacing-small) var(--spacing-medium);
|
||||||
|
@ -8880,10 +8890,19 @@ code .ss,
|
||||||
/* src/rawdata/scss/timeline.css */
|
/* src/rawdata/scss/timeline.css */
|
||||||
.avatar {
|
.avatar {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--dimmest-color);
|
background-color: var(--dimmest-color);
|
||||||
flex-shrink: 0;
|
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 {
|
.timeline-item {
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
|
|
|
@ -44,6 +44,8 @@ func FetchSnippets(
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
isFiltering := len(q.IDs) > 0 || len(q.Tags) > 0 || len(q.ProjectIDs) > 0
|
||||||
|
|
||||||
var tagSnippetIDs []int
|
var tagSnippetIDs []int
|
||||||
if len(q.Tags) > 0 {
|
if len(q.Tags) > 0 {
|
||||||
// Get snippet IDs with this tag, then use that in the main query
|
// 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, q.IDs...)
|
||||||
allSnippetIDs = append(allSnippetIDs, tagSnippetIDs...)
|
allSnippetIDs = append(allSnippetIDs, tagSnippetIDs...)
|
||||||
allSnippetIDs = append(allSnippetIDs, projectSnippetIDs...)
|
allSnippetIDs = append(allSnippetIDs, projectSnippetIDs...)
|
||||||
if len(allSnippetIDs) == 0 {
|
if isFiltering && len(allSnippetIDs) == 0 {
|
||||||
// We already managed to filter out all snippets, and all further
|
// We already managed to filter out all snippets, and all further
|
||||||
// parts of this query are more filters, so we can just fail everything
|
// parts of this query are more filters, so we can just fail everything
|
||||||
// else from right here.
|
// else from right here.
|
||||||
qb.Add(`AND FALSE`)
|
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)
|
qb.Add(`AND (snippet.id = ANY ($?) OR snippet.owner_id = ANY ($?))`, allSnippetIDs, q.OwnerIDs)
|
||||||
} else {
|
} else {
|
||||||
qb.Add(`AND snippet.id = ANY ($?)`, allSnippetIDs)
|
if len(allSnippetIDs) > 0 {
|
||||||
|
qb.Add(`AND snippet.id = ANY ($?)`, allSnippetIDs)
|
||||||
|
}
|
||||||
if len(q.OwnerIDs) > 0 {
|
if len(q.OwnerIDs) > 0 {
|
||||||
qb.Add(`AND snippet.owner_id = ANY ($?)`, q.OwnerIDs)
|
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"`
|
||||||
|
}
|
|
@ -441,7 +441,7 @@ pre,
|
||||||
.w7-ns {
|
.w7-ns {
|
||||||
width: var(--width-7);
|
width: var(--width-7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.w8-ns {
|
.w8-ns {
|
||||||
width: var(--width-8);
|
width: var(--width-8);
|
||||||
}
|
}
|
||||||
|
@ -527,7 +527,7 @@ pre,
|
||||||
.w7-m {
|
.w7-m {
|
||||||
width: var(--width-7);
|
width: var(--width-7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.w8-m {
|
.w8-m {
|
||||||
width: var(--width-8);
|
width: var(--width-8);
|
||||||
}
|
}
|
||||||
|
@ -609,7 +609,7 @@ pre,
|
||||||
.w7-l {
|
.w7-l {
|
||||||
width: var(--width-7);
|
width: var(--width-7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.w8-l {
|
.w8-l {
|
||||||
width: var(--width-8);
|
width: var(--width-8);
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
.svgicon {
|
||||||
svg {
|
svg {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
stroke: currentColor;
|
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
@ -697,6 +701,13 @@ pre,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.svgicon-lite {
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sr {
|
.sr {
|
||||||
border: 0;
|
border: 0;
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
|
|
|
@ -115,6 +115,8 @@ header.old {
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background-color: var(--bg-3);
|
background-color: var(--bg-3);
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
|
||||||
.hmn-logo {
|
.hmn-logo {
|
||||||
font-family: 'MohaveHMN', sans-serif;
|
font-family: 'MohaveHMN', sans-serif;
|
||||||
|
@ -128,11 +130,11 @@ header {
|
||||||
/* ensure that it also has .svgicon */
|
/* ensure that it also has .svgicon */
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: var(--spacing-extra-small);
|
margin-left: var(--spacing-extra-small);
|
||||||
font-size: var(--font-size-7);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 1.8rem;
|
width: 1.8rem;
|
||||||
|
height: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-nav {
|
.header-nav {
|
||||||
|
@ -150,6 +152,9 @@ header {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
min-width: 8rem;
|
min-width: 8rem;
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-top-width: 0;
|
||||||
|
|
||||||
>a {
|
>a {
|
||||||
padding: var(--spacing-small) var(--spacing-medium);
|
padding: var(--spacing-small) var(--spacing-medium);
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
.avatar {
|
.avatar {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--dimmest-color);
|
background-color: var(--dimmest-color);
|
||||||
flex-shrink: 0;
|
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 {
|
.timeline-item {
|
||||||
|
|
|
@ -56,8 +56,11 @@ $breakpoint-large: screen and (min-width: 60em)
|
||||||
|
|
||||||
--spoiler-border: #aaa;
|
--spoiler-border: #aaa;
|
||||||
|
|
||||||
--site-width: 80rem;
|
--site-width: 54rem;
|
||||||
--site-width-narrow: 60rem;
|
--site-width-narrow: 40rem;
|
||||||
|
|
||||||
|
--avatar-size-small: 1.5rem;
|
||||||
|
--avatar-size-normal: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
@ -112,4 +115,4 @@ $breakpoint-large: screen and (min-width: 60em)
|
||||||
|
|
||||||
--spoiler-border: #777;
|
--spoiler-border: #777;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"?>
|
<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;">
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
<g transform="matrix(3.30372e-18,0.0539539,-0.0539539,3.30372e-18,9.99997,4.04654e-05)">
|
||||||
<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)">
|
|
||||||
<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;"/>
|
<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>
|
</g>
|
||||||
</svg>
|
</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(
|
func ProjectToTemplate(
|
||||||
p *models.Project,
|
p *models.Project,
|
||||||
url string,
|
|
||||||
) Project {
|
) Project {
|
||||||
return Project{
|
return Project{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
|
@ -82,7 +81,7 @@ func ProjectToTemplate(
|
||||||
Subdomain: p.Subdomain(),
|
Subdomain: p.Subdomain(),
|
||||||
Color1: p.Color1,
|
Color1: p.Color1,
|
||||||
Color2: p.Color2,
|
Color2: p.Color2,
|
||||||
Url: url,
|
Url: hmndata.UrlContextForProject(p).BuildHomepage(),
|
||||||
Blurb: p.Blurb,
|
Blurb: p.Blurb,
|
||||||
ParsedDescription: template.HTML(p.ParsedDescription),
|
ParsedDescription: template.HTML(p.ParsedDescription),
|
||||||
|
|
||||||
|
@ -98,8 +97,8 @@ func ProjectToTemplate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProjectAndStuffToTemplate(p *hmndata.ProjectAndStuff, url string) Project {
|
func ProjectAndStuffToTemplate(p *hmndata.ProjectAndStuff) Project {
|
||||||
res := ProjectToTemplate(&p.Project, url)
|
res := ProjectToTemplate(&p.Project)
|
||||||
res.Logo = ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset)
|
res.Logo = ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset)
|
||||||
for _, o := range p.Owners {
|
for _, o := range p.Owners {
|
||||||
res.Owners = append(res.Owners, UserToTemplate(o))
|
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">
|
<a href="{{ .Header.HMNHomepageUrl }}" class="hmn-logo flex-shrink-0">
|
||||||
Handmade
|
Handmade
|
||||||
</a>
|
</a>
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
<a href="{{ .Header.JamsUrl }}">Jams</a>
|
<a href="{{ .Header.JamsUrl }}">Jams</a>
|
||||||
<div class="root-item">
|
<div class="root-item">
|
||||||
<a aria-expanded="false" aria-controls="events-submenu" class="menu-dropdown-js" href="#">
|
<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>
|
</a>
|
||||||
<div class="submenu" id="events-submenu">
|
<div class="submenu" id="events-submenu">
|
||||||
<a href="{{ .Header.JamsUrl }}">Jams</a>
|
<a href="{{ .Header.JamsUrl }}">Jams</a>
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="root-item">
|
<div class="root-item">
|
||||||
<a aria-expanded="false" aria-controls="resource-submenu" class="menu-dropdown-js" href="#">
|
<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>
|
</a>
|
||||||
<div class="submenu" id="resource-submenu">
|
<div class="submenu" id="resource-submenu">
|
||||||
<a href="{{ .Header.ForumsUrl }}">Forums</a>
|
<a href="{{ .Header.ForumsUrl }}">Forums</a>
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="root-item">
|
<div class="root-item">
|
||||||
<a aria-expanded="false" aria-controls="about-submenu" class="menu-dropdown-js" href="#">
|
<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>
|
</a>
|
||||||
<div class="submenu" id="about-submenu">
|
<div class="submenu" id="about-submenu">
|
||||||
<a href="{{ .Header.ManifestoUrl }}">Manifesto</a>
|
<a href="{{ .Header.ManifestoUrl }}">Manifesto</a>
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="db ph3 pv2 flex" href="{{ or .Header.UserProfileUrl .LoginPageUrl }}">
|
<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>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
{{ if .OwnerAvatarUrl }}
|
{{ if .OwnerAvatarUrl }}
|
||||||
<a class="flex flex-shrink-0" href="{{ .OwnerUrl }}">
|
<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>
|
</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
|
@ -84,29 +84,43 @@
|
||||||
<div class="flex justify-center pa3">
|
<div class="flex justify-center pa3">
|
||||||
<div class="w-100 mw-site flex g3">
|
<div class="w-100 mw-site flex g3">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="w5 flex flex-column g2">
|
<div class="w5 flex flex-column g2 flex-shrink-0">
|
||||||
<div class="bg--card pa2">
|
{{ if .User }}
|
||||||
<div class="pb2 flex justify-between items-center">
|
<div class="bg--card link--normal">
|
||||||
<span class="f7">Your projects</span>
|
<div class="pa2 flex justify-between items-center">
|
||||||
<span class="svgicon f8">{{ svg "chevron-down" }}</span>
|
<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>
|
||||||
<div>
|
<div class="bg--card link--normal">
|
||||||
You have not created any projects.
|
<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>
|
{{ end }}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feed -->
|
<!-- 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">
|
<div class="timeline flex flex-column g3">
|
||||||
{{ range .RecentItems }}
|
{{ range .RecentItems }}
|
||||||
{{ template "timeline_item.html" . }}
|
{{ template "timeline_item.html" . }}
|
||||||
|
@ -118,3 +132,38 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ end }}
|
{{ 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
|
Width, Height int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Follow struct {
|
||||||
|
User *User
|
||||||
|
Project *Project
|
||||||
|
}
|
||||||
|
|
||||||
type ProjectJamParticipation struct {
|
type ProjectJamParticipation struct {
|
||||||
JamName string
|
JamName string
|
||||||
JamSlug string
|
JamSlug string
|
||||||
|
|
|
@ -236,7 +236,7 @@ func AdminApprovalQueue(c *RequestContext) ResponseData {
|
||||||
userData.Date = p.ProjectAndStuff.Project.DateCreated
|
userData.Date = p.ProjectAndStuff.Project.DateCreated
|
||||||
}
|
}
|
||||||
userData.ProjectsWithLinks = append(userData.ProjectsWithLinks, projectWithLinks{
|
userData.ProjectsWithLinks = append(userData.ProjectsWithLinks, projectWithLinks{
|
||||||
Project: templates.ProjectAndStuffToTemplate(p.ProjectAndStuff, hmndata.UrlContextForProject(&p.ProjectAndStuff.Project).BuildHomepage()),
|
Project: templates.ProjectAndStuffToTemplate(p.ProjectAndStuff),
|
||||||
Links: projectLinks,
|
Links: projectLinks,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
|
||||||
DiscordInviteUrl: "https://discord.gg/hmn",
|
DiscordInviteUrl: "https://discord.gg/hmn",
|
||||||
NewsletterSignupUrl: hmnurl.BuildAPINewsletterSignup(),
|
NewsletterSignupUrl: hmnurl.BuildAPINewsletterSignup(),
|
||||||
|
|
||||||
Project: templates.ProjectToTemplate(&project, c.UrlContext.BuildHomepage()),
|
Project: templates.ProjectToTemplate(&project),
|
||||||
User: templateUser,
|
User: templateUser,
|
||||||
Session: templateSession,
|
Session: templateSession,
|
||||||
Notices: notices,
|
Notices: notices,
|
||||||
|
|
|
@ -165,7 +165,7 @@ func AtomFeed(c *RequestContext) ResponseData {
|
||||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects"))
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects"))
|
||||||
}
|
}
|
||||||
for _, p := range projectsAndStuff {
|
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()
|
templateProject.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(templateProject.Url)).URN()
|
||||||
for _, owner := range p.Owners {
|
for _, owner := range p.Owners {
|
||||||
templateProject.Owners = append(templateProject.Owners, templates.UserToTemplate(owner))
|
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))
|
projects := make([]templates.Project, 0, len(jamProjects))
|
||||||
for _, jp := range jamProjects {
|
for _, jp := range jamProjects {
|
||||||
urlContext := hmndata.UrlContextForProject(&jp.Project)
|
projects = append(projects, templates.ProjectAndStuffToTemplate(&jp))
|
||||||
projectUrl := urlContext.BuildHomepage()
|
|
||||||
projects = append(projects, templates.ProjectAndStuffToTemplate(&jp, projectUrl))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
projectIds := make([]int, 0, len(jamProjects))
|
projectIds := make([]int, 0, len(jamProjects))
|
||||||
|
@ -367,7 +365,7 @@ func JamIndex2023(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
||||||
for _, p := range 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))
|
projectIds := make([]int, 0, len(jamProjects))
|
||||||
|
@ -460,7 +458,7 @@ func JamFeed2023(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
||||||
for _, p := range 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 {
|
type JamFeedData struct {
|
||||||
|
@ -553,7 +551,7 @@ func JamIndex2023_Visibility(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
||||||
for _, p := range 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))
|
projectIds := make([]int, 0, len(jamProjects))
|
||||||
|
@ -628,7 +626,7 @@ func JamFeed2023_Visibility(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
||||||
for _, p := range 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 {
|
type JamFeedData struct {
|
||||||
|
@ -767,7 +765,7 @@ func JamIndex2022(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
||||||
for _, p := range 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))
|
projectIds := make([]int, 0, len(jamProjects))
|
||||||
|
@ -841,7 +839,7 @@ func JamFeed2022(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
pageProjects := make([]templates.Project, 0, len(jamProjects))
|
||||||
for _, p := range 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 {
|
type JamFeedData struct {
|
||||||
|
|
|
@ -17,6 +17,31 @@ func Index(c *RequestContext) ResponseData {
|
||||||
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
c.Perf.EndBlock()
|
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 err error
|
||||||
var followingItems []templates.TimelineItem
|
var followingItems []templates.TimelineItem
|
||||||
var featuredItems []templates.TimelineItem
|
var featuredItems []templates.TimelineItem
|
||||||
|
@ -59,25 +84,26 @@ func Index(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
type LandingTemplateData struct {
|
var projects []templates.Project
|
||||||
templates.BaseData
|
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
|
for _, p := range projectsDb {
|
||||||
FollowingItems []templates.TimelineItem
|
projects = append(projects, templates.ProjectAndStuffToTemplate(&p))
|
||||||
FeaturedItems []templates.TimelineItem
|
}
|
||||||
RecentItems []templates.TimelineItem
|
}
|
||||||
NewsItems []templates.TimelineItem
|
|
||||||
|
|
||||||
ManifestoUrl string
|
var follows []templates.Follow
|
||||||
PodcastUrl string
|
if c.CurrentUser != nil {
|
||||||
AtomFeedUrl string
|
follows, err = FetchFollows(c, c.Conn, c.CurrentUser, c.CurrentUser.ID)
|
||||||
MarkAllReadUrl string
|
if err != nil {
|
||||||
|
c.Logger.Warn().Err(err).Msg("failed to fetch user follows")
|
||||||
JamUrl string
|
}
|
||||||
JamDaysUntilStart, JamDaysUntilEnd int
|
|
||||||
|
|
||||||
HMSDaysUntilStart, HMSDaysUntilEnd int
|
|
||||||
HMBostonDaysUntilStart, HMBostonDaysUntilEnd int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
baseData := getBaseData(c, "", nil)
|
baseData := getBaseData(c, "", nil)
|
||||||
|
@ -96,10 +122,14 @@ func Index(c *RequestContext) ResponseData {
|
||||||
RecentItems: recentItems,
|
RecentItems: recentItems,
|
||||||
NewsItems: newsItems,
|
NewsItems: newsItems,
|
||||||
|
|
||||||
|
UserProjects: projects,
|
||||||
|
Following: follows,
|
||||||
|
|
||||||
ManifestoUrl: hmnurl.BuildManifesto(),
|
ManifestoUrl: hmnurl.BuildManifesto(),
|
||||||
PodcastUrl: hmnurl.BuildPodcast(),
|
PodcastUrl: hmnurl.BuildPodcast(),
|
||||||
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||||
MarkAllReadUrl: hmnurl.HMNProjectContext.BuildForumMarkRead(0),
|
MarkAllReadUrl: hmnurl.HMNProjectContext.BuildForumMarkRead(0),
|
||||||
|
NewProjectUrl: hmnurl.BuildProjectNew(),
|
||||||
|
|
||||||
JamUrl: hmnurl.BuildJamIndex2024_Learning(),
|
JamUrl: hmnurl.BuildJamIndex2024_Learning(),
|
||||||
JamDaysUntilStart: daysUntil(hmndata.LJ2024.StartTime),
|
JamDaysUntilStart: daysUntil(hmndata.LJ2024.StartTime),
|
||||||
|
|
|
@ -238,7 +238,7 @@ func getShuffledOfficialProjects(c *RequestContext) ([]templates.Project, error)
|
||||||
var restProjects []templates.Project
|
var restProjects []templates.Project
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, p := range official {
|
for _, p := range official {
|
||||||
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
|
templateProject := templates.ProjectAndStuffToTemplate(&p)
|
||||||
|
|
||||||
if p.Project.Slug == "hero" {
|
if p.Project.Slug == "hero" {
|
||||||
// NOTE(asaf): Handmade Hero gets special treatment. Must always be first in the list.
|
// 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
|
var personalProjects []templates.Project
|
||||||
for _, p := range projects {
|
for _, p := range projects {
|
||||||
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
|
templateProject := templates.ProjectAndStuffToTemplate(&p)
|
||||||
personalProjects = append(personalProjects, templateProject)
|
personalProjects = append(personalProjects, templateProject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,7 +410,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project details"))
|
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 {
|
for _, owner := range owners {
|
||||||
templateData.Owners = append(templateData.Owners, templates.UserToTemplate(owner))
|
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"))
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user projects"))
|
||||||
}
|
}
|
||||||
templateProjects := make([]templates.Project, 0, len(userProjects))
|
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 {
|
for _, p := range userProjects {
|
||||||
if p.Project.ID == c.CurrentProject.ID {
|
if p.Project.ID == c.CurrentProject.ID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
|
templateProject := templates.ProjectAndStuffToTemplate(&p)
|
||||||
templateProjects = append(templateProjects, templateProject)
|
templateProjects = append(templateProjects, templateProject)
|
||||||
}
|
}
|
||||||
templateData.SnippetEdit = templates.SnippetEdit{
|
templateData.SnippetEdit = templates.SnippetEdit{
|
||||||
|
|
|
@ -114,7 +114,7 @@ func Snippet(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
templateProjects := make([]templates.Project, 0, len(userProjects))
|
templateProjects := make([]templates.Project, 0, len(userProjects))
|
||||||
for _, p := range userProjects {
|
for _, p := range userProjects {
|
||||||
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
|
templateProject := templates.ProjectAndStuffToTemplate(&p)
|
||||||
templateProjects = append(templateProjects, templateProject)
|
templateProjects = append(templateProjects, templateProject)
|
||||||
}
|
}
|
||||||
snippetEdit = templates.SnippetEdit{
|
snippetEdit = templates.SnippetEdit{
|
||||||
|
|
|
@ -18,18 +18,14 @@ import (
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
"git.handmade.network/hmn/hmn/src/perf"
|
"git.handmade.network/hmn/hmn/src/perf"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"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) {
|
func FetchFollowTimelineForUser(ctx context.Context, conn db.ConnOrTx, user *models.User) ([]templates.TimelineItem, error) {
|
||||||
perf := perf.ExtractPerf(ctx)
|
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")
|
perf.StartBlock("FOLLOW", "Assemble follow data")
|
||||||
following, err := db.Query[Follower](ctx, conn, `
|
following, err := db.Query[models.Follow](ctx, conn, `
|
||||||
SELECT $columns
|
SELECT $columns
|
||||||
FROM follower
|
FROM follower
|
||||||
WHERE user_id = $1
|
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) {
|
func FetchTimeline(ctx context.Context, conn db.ConnOrTx, currentUser *models.User, q TimelineQuery) ([]templates.TimelineItem, error) {
|
||||||
perf := perf.ExtractPerf(ctx)
|
perf := perf.ExtractPerf(ctx)
|
||||||
var users []*models.User
|
// var users []*models.User
|
||||||
var projects []hmndata.ProjectAndStuff
|
// var projects []hmndata.ProjectAndStuff
|
||||||
var snippets []hmndata.SnippetAndStuff
|
var snippets []hmndata.SnippetAndStuff
|
||||||
var posts []hmndata.PostAndStuff
|
var posts []hmndata.PostAndStuff
|
||||||
var streamers []hmndata.TwitchStreamer
|
// var streamers []hmndata.TwitchStreamer
|
||||||
var streams []*models.TwitchStreamHistory
|
// var streams []*models.TwitchStreamHistory
|
||||||
|
var err error
|
||||||
|
|
||||||
perf.StartBlock("TIMELINE", "Fetch timeline data")
|
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{
|
snippets, err = hmndata.FetchSnippets(ctx, conn, currentUser, hmndata.SnippetQuery{
|
||||||
OwnerIDs: validUserIDs,
|
OwnerIDs: q.UserIDs,
|
||||||
ProjectIDs: validProjectIDs,
|
ProjectIDs: q.ProjectIDs,
|
||||||
|
|
||||||
|
Limit: q.Limit,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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{
|
posts, err = hmndata.FetchPosts(ctx, conn, currentUser, hmndata.PostsQuery{
|
||||||
UserIDs: validUserIDs,
|
UserIDs: q.UserIDs,
|
||||||
ProjectIDs: validProjectIDs,
|
ProjectIDs: q.ProjectIDs,
|
||||||
SortDescending: true,
|
SortDescending: true,
|
||||||
|
|
||||||
|
Limit: q.Limit,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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{
|
// streamers, err = hmndata.FetchTwitchStreamers(ctx, conn, hmndata.TwitchStreamersQuery{
|
||||||
UserIDs: validUserIDs,
|
// UserIDs: validUserIDs,
|
||||||
ProjectIDs: validProjectIDs,
|
// ProjectIDs: validProjectIDs,
|
||||||
})
|
// })
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, oops.New(err, "failed to fetch streamers")
|
// return nil, oops.New(err, "failed to fetch streamers")
|
||||||
}
|
// }
|
||||||
|
|
||||||
twitchLogins := make([]string, 0, len(streamers))
|
// twitchLogins := make([]string, 0, len(streamers))
|
||||||
for _, s := range streamers {
|
// for _, s := range streamers {
|
||||||
twitchLogins = append(twitchLogins, s.TwitchLogin)
|
// twitchLogins = append(twitchLogins, s.TwitchLogin)
|
||||||
}
|
// }
|
||||||
streams, err = db.Query[models.TwitchStreamHistory](ctx, conn,
|
// streams, err = db.Query[models.TwitchStreamHistory](ctx, conn,
|
||||||
`
|
// `
|
||||||
SELECT $columns FROM twitch_stream_history WHERE twitch_login = ANY ($1)
|
// SELECT $columns FROM twitch_stream_history WHERE twitch_login = ANY ($1)
|
||||||
`,
|
// `,
|
||||||
twitchLogins,
|
// twitchLogins,
|
||||||
)
|
// )
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, oops.New(err, "failed to fetch stream histories")
|
// return nil, oops.New(err, "failed to fetch stream histories")
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
perf.EndBlock()
|
perf.EndBlock()
|
||||||
|
|
||||||
|
@ -184,38 +152,38 @@ func FetchTimeline(ctx context.Context, conn db.ConnOrTx, currentUser *models.Us
|
||||||
timelineItems = append(timelineItems, item)
|
timelineItems = append(timelineItems, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range streams {
|
// for _, s := range streams {
|
||||||
ownerAvatarUrl := ""
|
// ownerAvatarUrl := ""
|
||||||
ownerName := ""
|
// ownerName := ""
|
||||||
ownerUrl := ""
|
// ownerUrl := ""
|
||||||
|
|
||||||
for _, streamer := range streamers {
|
// for _, streamer := range streamers {
|
||||||
if streamer.TwitchLogin == s.TwitchLogin {
|
// if streamer.TwitchLogin == s.TwitchLogin {
|
||||||
if streamer.UserID != nil {
|
// if streamer.UserID != nil {
|
||||||
for _, u := range users {
|
// for _, u := range users {
|
||||||
if u.ID == *streamer.UserID {
|
// if u.ID == *streamer.UserID {
|
||||||
ownerAvatarUrl = templates.UserAvatarUrl(u)
|
// ownerAvatarUrl = templates.UserAvatarUrl(u)
|
||||||
ownerName = u.BestName()
|
// ownerName = u.BestName()
|
||||||
ownerUrl = hmnurl.BuildUserProfile(u.Username)
|
// ownerUrl = hmnurl.BuildUserProfile(u.Username)
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else if streamer.ProjectID != nil {
|
// } else if streamer.ProjectID != nil {
|
||||||
for _, p := range projects {
|
// for _, p := range projects {
|
||||||
if p.Project.ID == *streamer.ProjectID {
|
// if p.Project.ID == *streamer.ProjectID {
|
||||||
ownerAvatarUrl = templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset)
|
// ownerAvatarUrl = templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset)
|
||||||
ownerName = p.Project.Name
|
// ownerName = p.Project.Name
|
||||||
ownerUrl = hmndata.UrlContextForProject(&p.Project).BuildHomepage()
|
// ownerUrl = hmndata.UrlContextForProject(&p.Project).BuildHomepage()
|
||||||
}
|
// }
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
item := TwitchStreamToTimelineItem(s, ownerAvatarUrl, ownerName, ownerUrl)
|
// item := TwitchStreamToTimelineItem(s, ownerAvatarUrl, ownerName, ownerUrl)
|
||||||
timelineItems = append(timelineItems, item)
|
// timelineItems = append(timelineItems, item)
|
||||||
}
|
// }
|
||||||
|
|
||||||
perf.StartBlock("TIMELINE", "Sort timeline")
|
perf.StartBlock("TIMELINE", "Sort timeline")
|
||||||
sort.Slice(timelineItems, func(i, j int) bool {
|
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()
|
||||||
perf.EndBlock()
|
perf.EndBlock()
|
||||||
|
|
||||||
|
if q.Limit > 0 {
|
||||||
|
timelineItems = utils.ClampSlice(timelineItems, q.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
return timelineItems, nil
|
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 {
|
type TimelineTypeTitles struct {
|
||||||
TypeTitleFirst string
|
TypeTitleFirst string
|
||||||
TypeTitleNotFirst string
|
TypeTitleNotFirst string
|
||||||
|
@ -369,7 +413,7 @@ func SnippetToTimelineItem(
|
||||||
return projects[i].Project.Name < projects[j].Project.Name
|
return projects[i].Project.Name < projects[j].Project.Name
|
||||||
})
|
})
|
||||||
for _, proj := range projects {
|
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
|
return item
|
||||||
|
|
|
@ -106,7 +106,7 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
templateProjects := make([]templates.Project, 0, len(projectsAndStuff))
|
templateProjects := make([]templates.Project, 0, len(projectsAndStuff))
|
||||||
numPersonalProjects := 0
|
numPersonalProjects := 0
|
||||||
for _, p := range projectsAndStuff {
|
for _, p := range projectsAndStuff {
|
||||||
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
|
templateProject := templates.ProjectAndStuffToTemplate(&p)
|
||||||
templateProjects = append(templateProjects, templateProject)
|
templateProjects = append(templateProjects, templateProject)
|
||||||
|
|
||||||
if p.Project.Personal {
|
if p.Project.Personal {
|
||||||
|
|
Loading…
Reference in New Issue