Rework the home page

This commit is contained in:
Ben Visness 2021-10-24 15:48:28 -05:00
parent 7d422cb533
commit f8d5f9fce5
18 changed files with 363 additions and 687 deletions

View File

@ -113,3 +113,106 @@ function makeShowcaseItem(timelineItem) {
return [itemEl, doOnce(addThumbnailFunc)]; return [itemEl, doOnce(addThumbnailFunc)];
} }
function initShowcaseContainer(container, items, rowHeight = 300, itemSpacing = 4) {
const addThumbnailFuncs = new Array(items.length);
const itemElements = []; // array of arrays
for (let i = 0; i < items.length; i++) {
const item = items[i];
const [itemEl, addThumbnail] = makeShowcaseItem(item);
itemEl.container.setAttribute('data-index', i);
itemEl.container.setAttribute('data-date', item.date);
addThumbnailFuncs[i] = addThumbnail;
itemElements.push(itemEl.container);
}
function layout() {
const width = container.getBoundingClientRect().width;
container = emptyElement(container);
function addRow(itemEls, rowWidth, container) {
const totalSpacing = itemSpacing * (itemEls.length - 1);
const scaleFactor = (width / Math.max(rowWidth, width));
const row = document.createElement('div');
row.classList.add('flex');
row.classList.toggle('justify-between', rowWidth >= width);
row.style.marginBottom = `${itemSpacing}px`;
for (const itemEl of itemEls) {
const index = parseInt(itemEl.getAttribute('data-index'), 10);
const item = items[index];
const aspect = item.width / item.height;
const baseWidth = (aspect * rowHeight) * scaleFactor;
const actualWidth = baseWidth - (totalSpacing / itemEls.length);
itemEl.style.width = `${actualWidth}px`;
itemEl.style.height = `${scaleFactor * rowHeight}px`;
itemEl.style.marginRight = `${itemSpacing}px`;
row.appendChild(itemEl);
}
container.appendChild(row);
}
let rowItemEls = [];
let rowWidth = 0;
for (const itemEl of itemElements) {
const index = parseInt(itemEl.getAttribute('data-index'), 10);
const item = items[index];
const aspect = item.width / item.height;
rowWidth += aspect * rowHeight;
rowItemEls.push(itemEl);
if (rowWidth > width) {
addRow(rowItemEls, rowWidth, container);
rowItemEls = [];
rowWidth = 0;
}
}
addRow(rowItemEls, rowWidth, container);
}
function tryLoadImages() {
const OFFSCREEN_THRESHOLD = 0;
const rect = container.getBoundingClientRect();
const offscreen = (
rect.bottom < -OFFSCREEN_THRESHOLD
|| rect.top > window.innerHeight + OFFSCREEN_THRESHOLD
);
if (!offscreen) {
const items = container.querySelectorAll('.showcase-item');
for (const item of items) {
const i = parseInt(item.getAttribute('data-index'), 10);
addThumbnailFuncs[i]();
}
}
}
window.addEventListener('DOMContentLoaded', () => {
layout();
layout(); // scrollbars are fun!!
tryLoadImages();
})
window.addEventListener('resize', () => {
layout();
tryLoadImages();
});
window.addEventListener('scroll', () => {
tryLoadImages();
});
}

View File

@ -7337,6 +7337,12 @@ article code {
flex-grow: 1; flex-grow: 1;
flex-shrink: 1; } } flex-shrink: 1; } }
@media screen and (min-width: 60em) {
.flex-fair-l {
flex-basis: 1px;
flex-grow: 1;
flex-shrink: 1; } }
.b--theme { .b--theme {
border-color: #666; border-color: #666;
border-color: var(--theme-color); } border-color: var(--theme-color); }
@ -7397,6 +7403,10 @@ article code {
background-color: #f8f8f8; background-color: #f8f8f8;
background-color: var(--content-background); } background-color: var(--content-background); }
.bg--card {
background-color: #e8e8e8;
background-color: var(--card-background); }
.f8 { .f8 {
font-size: 0.65rem; } font-size: 0.65rem; }
@ -9016,66 +9026,21 @@ span.icon-rss::before {
.chat #users .user { .chat #users .user {
cursor: pointer; } cursor: pointer; }
.landing .breadcrumb { .landing-layout {
padding: 0px 3px; } display: grid;
.landing .breadcrumb:first-child { gap: 1rem; }
padding-left: 0px; } .landing-layout > * {
overflow: hidden; }
.landing .more { @media screen and (min-width: 60em) {
display: block; .landing-layout {
margin-top: 10px; } grid-template-columns: 1fr;
grid-auto-columns: 1fr; }
.landing .contents { .landing-layout > * {
margin-bottom: 20px; } grid-column: 1 / 2; }
.landing-layout > *.landing-right {
.landing .showcase .arrow-container { grid-column: 2 / 3;
width: 60px; grid-row: 1 / 20; } }
position: absolute;
top: 0;
bottom: 0;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center; }
.landing .showcase .arrow-container.left {
left: 0;
background-image: linear-gradient(to right, #f8f8f8 , rgba(0, 0, 0, 0));
background-image: linear-gradient(to right, var(--content-background) , rgba(0, 0, 0, 0)); }
.landing .showcase .arrow-container.left svg {
transform: translateX(-0.05rem); }
.landing .showcase .arrow-container.right {
right: 0;
background-image: linear-gradient(to left, #f8f8f8 , rgba(0, 0, 0, 0));
background-image: linear-gradient(to left, var(--content-background) , rgba(0, 0, 0, 0)); }
.landing .showcase .arrow-container.right svg {
transform: translateX(0.05rem); }
.landing .showcase .arrow-container.hide {
opacity: 0;
pointer-events: none; }
.landing .showcase .arrow-container .arrow {
background-color: #f8f8f8;
background-color: var(--content-background);
border-radius: 100%;
width: 2.4rem;
height: 2.4rem;
font-size: 1rem;
transition: opacity 40ms ease-in-out;
box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.2);
display: flex;
justify-content: center;
align-items: center; }
.landing #showcase-items {
transition: transform 200ms ease-in-out; }
.landing .showcase-item {
width: 7rem;
height: 7rem; }
@media screen and (min-width: 30em) {
.landing .showcase-item {
width: 10rem;
height: 10rem; } }
.star-btn { .star-btn {
border-bottom-width: 2px; border-bottom-width: 2px;
@ -9341,8 +9306,12 @@ span.icon-rss::before {
border-color: #666; border-color: #666;
border-color: var(--theme-color); } border-color: var(--theme-color); }
.timeline-item .avatar-icon.big { .timeline-item .avatar-icon.big {
width: 3.875rem; width: 3rem;
height: 3.875rem; } height: 3rem; }
@media screen and (min-width: 30em) {
.timeline-item .avatar-icon.big {
width: 3.875rem;
height: 3.875rem; } }
.timeline-item .timeline-content-box > * { .timeline-item .timeline-content-box > * {
display: block; display: block;
max-width: 100%; max-width: 100%;

View File

@ -285,8 +285,8 @@ pre, code, .codeblock {
--forum-diff-delete-border-color: #6b1e1c; --forum-diff-delete-border-color: #6b1e1c;
--forum-diff-insert-background: #233a18; --forum-diff-insert-background: #233a18;
--forum-diff-insert-border-color: #30591b; --forum-diff-insert-border-color: #30591b;
--card-background: #222; --card-background: #282828;
--card-background-hover: #282828; --card-background-hover: #333;
--irc-border-color: #333; --irc-border-color: #333;
--irc-tab-current-shadow: 0px 0px 5px #000 inset; --irc-tab-current-shadow: 0px 0px 5px #000 inset;
--irc-tab-close-button-color: #bbb; --irc-tab-close-button-color: #bbb;

View File

@ -189,6 +189,14 @@ article code {
} }
} }
@media #{$breakpoint-large} {
.flex-fair-l {
flex-basis: 1px;
flex-grow: 1;
flex-shrink: 1;
}
}
.b--theme { .b--theme {
@include usevar(border-color, theme-color); @include usevar(border-color, theme-color);
} }
@ -249,6 +257,10 @@ article code {
@include usevar(background-color, content-background); @include usevar(background-color, content-background);
} }
.bg--card {
@include usevar(background-color, card-background);
}
.f8 { .f8 {
font-size: 0.65rem; font-size: 0.65rem;
} }

View File

@ -1,86 +1,24 @@
.landing { .landing-layout {
.breadcrumb { display: grid;
padding: 0px 3px; gap: $spacing-medium;
&:first-child { > * {
padding-left: 0px; overflow: hidden;
}
} }
}
.more { @media #{$breakpoint-large} {
display: block; .landing-layout {
margin-top: 10px; grid-template-columns: 1fr;
} grid-auto-columns: 1fr;
.contents { > * {
margin-bottom: 20px; grid-column: 1 / 2;
}
.showcase { &.landing-right {
.arrow-container { grid-column: 2 / 3;
width: 60px; grid-row: 1 / 20; // increase this number if somehow you ever add that much garbage to the home page :)
position: absolute;
top: 0;
bottom: 0;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
$svg-scoot: 0.05rem;
&.left {
left: 0;
@include usevar(background-image, 'linear-gradient(to right, ' content-background ', rgba(0, 0, 0, 0))');
svg {
transform: translateX(-$svg-scoot);
}
} }
&.right {
right: 0;
@include usevar(background-image, 'linear-gradient(to left, ' content-background ', rgba(0, 0, 0, 0))');
svg {
transform: translateX($svg-scoot);
}
}
&.hide {
opacity: 0;
pointer-events: none;
}
.arrow {
@include usevar(background-color, content-background);
border-radius: 100%;
width: 2.4rem;
height: 2.4rem;
font-size: 1rem;
transition: opacity 40ms ease-in-out;
box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.20);
display: flex;
justify-content: center;
align-items: center;
}
}
}
#showcase-items {
transition: transform 200ms ease-in-out;
}
.showcase-item {
width: 7rem;
height: 7rem;
@media #{$breakpoint-not-small} {
width: 10rem;
height: 10rem;
} }
} }
} }

View File

@ -8,8 +8,13 @@
@include usevar(border-color, theme-color); @include usevar(border-color, theme-color);
&.big { &.big {
width: px2rem(62px); width: 3rem;
height: px2rem(62px); height: 3rem;
@media #{$breakpoint-not-small} {
width: px2rem(62px);
height: px2rem(62px);
}
} }
} }

View File

@ -95,8 +95,8 @@ $vars: (
forum-diff-insert-background: #233a18, forum-diff-insert-background: #233a18,
forum-diff-insert-border-color: #30591b, forum-diff-insert-border-color: #30591b,
card-background: #222, card-background: #282828,
card-background-hover: #282828, card-background-hover: #333,
irc-border-color: #333, irc-border-color: #333,
irc-tab-current-shadow: 0px 0px 5px #000 inset, irc-tab-current-shadow: 0px 0px 5px #000 inset,

View File

@ -249,6 +249,7 @@ func TimelineItemsToJSON(items []TimelineItem) string {
builder := strings.Builder{} builder := strings.Builder{}
builder.WriteRune('[') builder.WriteRune('[')
for i, item := range items { for i, item := range items {
if i > 0 { if i > 0 {
builder.WriteRune(',') builder.WriteRune(',')
} }

View File

@ -4,7 +4,7 @@
<div class="content-block"> <div class="content-block">
<div class="optionbar"> <div class="optionbar">
<div class="options"> <div class="options">
<a class="button" href="{{ .AtomFeedUrl }}"><span class="icon big pr1">4</span> RSS Feed</span></a> <a class="button" href="{{ .AtomFeedUrl }}"><span class="icon big pr1">4</span> RSS Feed</a>
{{ if .User }} {{ if .User }}
<form method="POST" action="{{ .MarkAllReadUrl }}"> <form method="POST" action="{{ .MarkAllReadUrl }}">
{{ csrftoken .Session }} {{ csrftoken .Session }}

View File

@ -1,18 +1,20 @@
<script src="{{ static "js/showcase.js" }}"></script>
<template id="showcase_item"> <template id="showcase_item">
<div data-tmpl="container" class="showcase-item ba b--theme flex-shrink-0 bg-dark-gray hide-child relative overflow-hidden pointer"> <div data-tmpl="container" class="showcase-item ba b--theme flex-shrink-0 bg-dark-gray hide-child relative overflow-hidden pointer">
<div data-tmpl="thumbnail" class="absolute absolute--fill z-0 flex justify-start items-center bg-left cover"></div> <div data-tmpl="thumbnail" class="absolute absolute--fill z-0 flex justify-start items-center bg-left cover"></div>
<div class="overlay absolute absolute--fill z-1 child"> <div class="overlay absolute absolute--fill z-1 child">
<div class="gradient relative"> <div class="gradient relative">
<div class="user-info flex pa2 white f7 lh-title items-center"> <div class="user-info flex pa2 white f7 lh-title items-center">
<div data-tmpl="avatar" class="br-100 w2 h2 cover flex-shrink-0"></div> <div data-tmpl="avatar" class="br-100 w2 h2 cover flex-shrink-0"></div>
<div class="flex-grow-1 flex flex-column pl1"> <div class="flex-grow-1 flex flex-column pl1">
<div data-tmpl="username">Unknown User</div> <div data-tmpl="username">Unknown User</div>
<div data-tmpl="when" class="i f8">Unknown Time</div> <div data-tmpl="when" class="i f8">Unknown Time</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<template id="timeline_modal"> <template id="timeline_modal">

View File

@ -1,10 +1,8 @@
{{ $small := or .Description .EmbedMedia }}
<div class="timeline-item flex flex-column pa3 mb2 br3" {{ with .FilterTitle }}data-filter-title="{{ . }}"{{ end }}> <div class="timeline-item flex flex-column pa3 mb2 br3" {{ with .FilterTitle }}data-filter-title="{{ . }}"{{ end }}>
{{/* top bar - avatar, info, date */}} {{/* top bar - avatar, info, date */}}
<div class="flex items-center"> <div class="flex items-center">
<a class="flex-shrink-0" href="{{ .OwnerUrl }}"> <a class="flex-shrink-0" href="{{ .OwnerUrl }}">
<img class="avatar-icon lite {{ if not $small }}big{{ end }} {{ if $small }}mr2{{ else }}mr3{{ end }}" src="{{ .OwnerAvatarUrl }}" /> <img class="avatar-icon lite {{ if not .SmallInfo }}big{{ end }} {{ if .SmallInfo }}mr2{{ else }}mr3{{ end }}" src="{{ .OwnerAvatarUrl }}" />
</a> </a>
<div class="overflow-hidden flex-grow-1 flex flex-column justify-center"> <div class="overflow-hidden flex-grow-1 flex flex-column justify-center">
@ -13,19 +11,19 @@
{{ end }} {{ end }}
{{ if .Title }} {{ if .Title }}
<div class="f5 nowrap truncate"> <div class="f5 nowrap truncate">
{{ with .TypeTitle }}<b>{{ . }}:</b>{{ end }} {{ with .TypeTitle }}<b class="dn di-ns">{{ . }}:</b>{{ end }}
<a href="{{ .Url }}">{{ .Title }}</a> <a href="{{ .Url }}">{{ .Title }}</a>
</div> </div>
{{ end }} {{ end }}
<div class="details"> <div class="details">
<a class="user" href="{{ .OwnerUrl }}">{{ .OwnerName }}</a> <a class="user" href="{{ .OwnerUrl }}">{{ .OwnerName }}</a>
{{ if not $small }} {{ if not .SmallInfo }}
&mdash; {{ timehtml (relativedate .Date) .Date }} &mdash; {{ timehtml (relativedate .Date) .Date }}
{{ end }} {{ end }}
</div> </div>
</div> </div>
{{ if $small }} {{ if .SmallInfo }}
<a href="{{ .Url }}">{{ timehtml (relativedate .Date) .Date }}</a> <a href="{{ .Url }}">{{ timehtml (relativedate .Date) .Date }}</a>
{{ end }} {{ end }}
</div> </div>
@ -39,6 +37,11 @@
<div class="excerpt-fade absolute w-100 h4 bottom-0 z-999"></div> <div class="excerpt-fade absolute w-100 h4 bottom-0 z-999"></div>
{{ end }} {{ end }}
</div> </div>
{{ if .TruncateDescription }}
<div class="mt2">
<a href="{{ .Url }}">Read more »</a>
</div>
{{ end }}
{{ end }} {{ end }}
{{ range .EmbedMedia }} {{ range .EmbedMedia }}

View File

@ -1,26 +1,7 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "extrahead" }} {{ define "extrahead" }}
<style type="text/css">
{{ $base := . }}
{{ range $col := .PostColumns }}
{{ range $entry := $col }}
{{ $c1 := hex2color .Project.Color1 }}
{{ $linkColor := eq $base.Theme "dark" | ternary (lightness 0.55 $c1) (lightness 0.35 $c1) | color2css }}
{{ $linkHoverColor := eq $base.Theme "dark" | ternary (lightness 0.65 $c1) (lightness 0.45 $c1) | color2css }}
{{ $projectPostBackground := eq $base.Theme "dark" | ternary (lightness 0.15 $c1) (lightness 0.95 $c1) | alpha 0.2 | color2css }}
#p{{ .Project.Subdomain }} a.project-title { color: {{ $linkColor }}; }
#p{{ .Project.Subdomain }} .unread a { color: {{ $linkColor }}; }
#p{{ .Project.Subdomain }} .unread a:hover { color: {{ $linkHoverColor }} }
#p{{ .Project.Subdomain }} .unread .avatar-icon { border-color: {{ $linkColor }}; }
#p{{ .Project.Subdomain }} .post-list-item:nth-of-type(even) { background-color: {{ $projectPostBackground }}; }
#p{{ .Project.Subdomain }} .thread.more { background-color:transparent; }
{{ end }}
{{ end }}
</style>
<script src="{{ static "js/templates.js" }}"></script> <script src="{{ static "js/templates.js" }}"></script>
<script src="{{ static "js/showcase.js" }}"></script>
{{ end }} {{ end }}
{{ define "content" }} {{ define "content" }}
@ -77,186 +58,63 @@
</div> </div>
</a> </a>
</div> </div>
{{ template "showcase_templates.html" }}
{{ if .ShowcaseTimelineJson }}
<div class="content-block pb3">
<div class="tc tl-l w-100 pb2">
<h2 class="di-l mr2-l">Community Showcase</h2>
<ul class="list dib-l">
<li class="dib-ns ma0 ph2">
<a href="{{ .ShowcaseUrl }}">View all</a>
</li>
</ul>
</div>
<div class="showcase relative overflow-hidden"> <div class="landing-layout ph3 ph0-ns">
<div id="showcase-items" class="flex relative pl3 pl0-ns"></div> {{/*
<div class="arrow-container left"> The order of the grid children should be as desired on mobile, then adapted to larger
<a href="javascript:void(0)" class="arrow svgicon svgicon-nofix" onclick="scrollShowcase('left')">{{ svg "chevron-left" }}</a> sizes using CSS grid properties.
*/}}
<div>
<h2>Latest News</h2>
{{ template "timeline_item.html" .NewsPost }}
</div>
<div class="landing-right">
<h2>Around the Network</h2>
<div class="optionbar mb2">
<div class="options">
<a class="button" href="{{ .AtomFeedUrl }}"><span class="icon big pr1">4</span> RSS Feed</a>
{{ if .User }}
<form method="POST" action="{{ .MarkAllReadUrl }}">
{{ csrftoken .Session }}
<button type="submit"><span class="big pr1">&#x2713;</span> Mark all posts on site as read</button>
</form>
{{ end }}
</div> </div>
<div class="arrow-container right"> <div class="options">
<a href="javascript:void(0)" class="arrow svgicon svgicon-nofix" onclick="scrollShowcase('right')">{{ svg "chevron-right" }}</a> {{ template "pagination.html" .Pagination }}
</div> </div>
</div> </div>
{{ range .TimelineItems }}
<div class="c--dimmer i pv2 ph3 ph0-ns"> {{ template "timeline_item.html" . }}
This is a selection of recent work done by community members. Want to participate? <a href="{{ .DiscordUrl }}" target="_blank">Join us on Discord.</a> {{ end }}
<div class="optionbar bottom mt2">
<div class="options"></div>
<div class="options">
{{ template "pagination.html" .Pagination }}
</div>
</div> </div>
</div> </div>
<script> {{ if .ShowcaseTimelineJson }}
const timelineData = JSON.parse("{{ .ShowcaseTimelineJson }}"); <div>
{{ template "showcase_templates.html" }}
const showcaseEl = document.querySelector('#showcase-items'); <div>
for (const item of timelineData) { <h2>Community Showcase</h2>
const [itemEl, addThumbnail] = makeShowcaseItem(item); <div class="bg--card pa3 br3">
addThumbnail(); <div class="mb3">
itemEl.container.classList.add('mr3'); This is a selection of recent work done by community members. Want to participate? <a href="{{ .DiscordUrl }}" target="_blank">Join us on Discord.</a>
showcaseEl.appendChild(itemEl.root); </div>
} <div id="showcase-container"></div>
<div>
function rem2px(rem) { <a class="db w-100 tc pa2" href="{{ .ShowcaseUrl }}">View all »</a>
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); </div>
} </div>
</div>
function scrollShowcase(direction = null) { <script>
const ITEM_WIDTH = showcaseEl.querySelector('.showcase-item').getBoundingClientRect().width; const showcaseItems = JSON.parse("{{ .ShowcaseTimelineJson }}");
const ITEM_SPACING = rem2px(1); initShowcaseContainer(document.querySelector('#showcase-container'), showcaseItems, 200);
</script>
const showcaseWidth = showcaseEl.getBoundingClientRect().width; </div>
const numVisible = showcaseWidth / (ITEM_WIDTH + ITEM_SPACING); {{ end }}
const scrollMagnitude = Math.floor(numVisible) - 1;
const scrollDirection = (direction === 'right' ? 1 : (direction === 'left' ? -1 : 0));
const scrollAmount = scrollMagnitude * scrollDirection;
const minIndex = 0;
const maxIndex = timelineData.length - Math.floor(numVisible);
const currentScrollIndex = parseInt(showcaseEl.getAttribute('data-scroll-index'), 10) || 0;
const newScrollIndex = Math.max(minIndex, Math.min(maxIndex, currentScrollIndex + scrollAmount));
showcaseEl.style.transform = `translateX(${-newScrollIndex * (ITEM_WIDTH + ITEM_SPACING)}px)`;
showcaseEl.setAttribute('data-scroll-index', newScrollIndex);
const leftArrowEl = document.querySelector('.arrow-container.left');
const rightArrowEl = document.querySelector('.arrow-container.right');
leftArrowEl.classList.toggle('hide', newScrollIndex === minIndex);
rightArrowEl.classList.toggle('hide', newScrollIndex === maxIndex);
}
scrollShowcase(); // force a scroll as an easy way to initialize styles
window.addEventListener('resize', () => scrollShowcase());
</script>
{{ end }}
<div class="content-block">
<div class="optionbar pb2">
<div class="tc tl-l w-100">
<h2 class="di-l mr2-l">Around the Network</h2>
<ul class="list dib-l">
<li class="dib-ns ma0 ph2">
<a href="{{ .FeedUrl }}">View all posts on HMN</a>
</li>
<li class="dib-ns ma0 ph2">
<a href="{{ .PodcastUrl }}">Podcast</a>
</li>
{{/* TODO: Make a better IRC intro page because the current one is trash anyway */}}
{{/*
<li class="dib-ns ma0 ph2">
<a href="{{ .StreamsUrl }}">See who's live</a>
</li>
<li class="dib-ns ma0 ph2">
<a href="{{ .IRCUrl }}" target="_blank">Chat in IRC</a>
</li>
*/}}
<li class="dib-ns ma0 ph2">
<a href="{{ .DiscordUrl }}" target="_blank">Chat on Discord</a>
</li>
<li class="dib-ns ma0 ph2">
<a href="{{ .ShowUrl }}" target="_blank">See the Show</a>
</li>
</ul>
</div>
</div>
</div>
<div class="content-block news cf">
{{ $newsPost := .NewsPost }}
{{ range $i, $col := .PostColumns }}
<div class="fl w-100 w-50-l">
<div class="mw7 mw-none-l center-layout">
{{ if eq $i 0 }}
<div class="pt3">
{{ template "landing_page_featured_post" $newsPost}}
</div>
{{ end }}
{{ range $entry := $col }}
{{ $proj := $entry.Project }}
{{ $posts := $entry.Posts }}
<div class="pt3" id="p{{ $proj.Subdomain }}">
{{ $c1 := hex2color $proj.Color1 }}
<a
class="project-title"
href="{{ $proj.Url }}"
>
<h2 class="ph3">{{ $proj.Name }}</h2>
</a>
{{ with $entry.FeaturedPost }}
{{ template "landing_page_featured_post" . }}
{{ end }}
{{ range $post := $posts }}
{{ template "post_list_item" $post }}
{{ end }}
<div class="ph3 thread unread more">
<a class="title" href="{{ $entry.ForumsUrl }}">
More posts &rarr;
</a>
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}
</div>
{{ end }}
{{ define "landing_page_featured_post" }}
{{/* Call this template with a LandingPageFeaturedPost. */}}
<div class="flex items-start ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }}">
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
<div class="flex-grow-1 overflow-hidden">
<div class="title mb1"><a href="{{ .Url }}">{{ .Title }}</a></div>
<div class="details">
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> &mdash; {{ timehtml (relativedate .Date) .Date }}
</div>
<div class="overflow-hidden mh-5 mt2 relative">
<div>
{{ .Content }}
</div>
<div class="excerpt-fade absolute w-100 h4 bottom-0 z-999"></div>
</div>
<div class="mt2">
<a href="{{ .Url }}">Read More &rarr;</a>
</div>
</div>
</div>
{{ end }}
{{ define "post_list_item" }}
{{/* Call this template with a PostListItem. */}}
<div class="post-list-item flex items-center ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }} {{ .Classes }}">
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
<div class="flex-grow-1 overflow-hidden">
<div class="title nowrap truncate"><a href="{{ .Url }}" title="{{ .Preview }}">{{ .Title }}</a></div>
<div class="details">
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> &mdash; {{ timehtml (relativedate .Date) .Date }}
</div>
</div>
<div class="goto">
<a href="{{ .Url }}">&raquo;</a>
</div>
</div> </div>
{{ end }} {{ end }}

View File

@ -2,10 +2,11 @@
{{ define "extrahead" }} {{ define "extrahead" }}
<script src="{{ static "js/templates.js" }}"></script> <script src="{{ static "js/templates.js" }}"></script>
<script src="{{ static "js/showcase.js" }}"></script>
{{ end }} {{ end }}
{{ define "content" }} {{ define "content" }}
{{ template "showcase_templates.html" }}
<div class="content-block"> <div class="content-block">
<div class="ph2 ph0-ns pb4"> <div class="ph2 ph0-ns pb4">
<div class="optionbar"> <div class="optionbar">
@ -22,150 +23,50 @@
</div> </div>
</div> </div>
{{ template "showcase_templates.html" }}
<template id="showcase-month"> <template id="showcase-month">
<h3 data-tmpl="dateHeader" class="mt3 f4 fw5">Unknown Date</h3> <h3 data-tmpl="dateHeader" class="mt3 f4 fw5">Unknown Date</h3>
<div data-tmpl="itemsContainer" class="month-container"></div> <div data-tmpl="itemsContainer" class="month-container"></div>
</template> </template>
<script> <script>
const ROW_HEIGHT = 300; const showcaseItems = JSON.parse("{{ .ShowcaseItems }}");
const ITEM_SPACING = 4;
const monthTemplate = makeTemplateCloner('showcase-month'); const monthTemplate = makeTemplateCloner('showcase-month');
const showcaseContainer = document.querySelector('#showcase-container');
const showcaseItems = JSON.parse("{{ .ShowcaseItems }}"); const itemsByMonth = []; // array of arrays
const addThumbnailFuncs = new Array(showcaseItems.length);
let showcaseContainer = document.querySelector('#showcase-container');
const itemElementsByMonth = []; // array of arrays
let currentMonthElements = [];
let currentMonth = null; let currentMonth = null;
let currentYear = null; let currentYear = null;
for (let i = 0; i < showcaseItems.length; i++) { let currentMonthItems = [];
const item = showcaseItems[i]; for (const item of showcaseItems) {
const date = new Date(item.date * 1000); const date = new Date(item.date * 1000);
if (date.getMonth() !== currentMonth || date.getFullYear() !== currentYear) { if (date.getMonth() !== currentMonth || date.getFullYear() !== currentYear) {
if (currentMonthElements.length > 0) { // rolled over to new month
itemElementsByMonth.push(currentMonthElements); if (currentMonthItems.length > 0) {
itemsByMonth.push(currentMonthItems);
} }
currentMonthElements = [];
currentMonth = date.getMonth(); currentMonth = date.getMonth();
currentYear = date.getFullYear(); currentYear = date.getFullYear();
currentMonthItems = [];
} }
const [itemEl, addThumbnail] = makeShowcaseItem(item); currentMonthItems.push(item);
itemEl.container.setAttribute('data-index', i);
itemEl.container.setAttribute('data-date', item.date);
addThumbnailFuncs[i] = addThumbnail;
currentMonthElements.push(itemEl.container);
} }
if (currentMonthElements.length > 0) { if (currentMonthItems.length > 0) {
itemElementsByMonth.push(currentMonthElements); itemsByMonth.push(currentMonthItems);
} }
function layout() { for (const monthItems of itemsByMonth) {
const width = showcaseContainer.getBoundingClientRect().width; const month = monthTemplate();
showcaseContainer = emptyElement(showcaseContainer);
function addRow(itemEls, rowWidth, container) { const firstDate = new Date(monthItems[0].date * 1000);
const totalSpacing = ITEM_SPACING * (itemEls.length - 1); month.dateHeader.textContent = firstDate.toLocaleDateString([], { month: 'long', year: 'numeric' });
const scaleFactor = (width / Math.max(rowWidth, width));
const row = document.createElement('div'); initShowcaseContainer(month.root.querySelector('.month-container'), monthItems);
row.classList.add('flex'); showcaseContainer.appendChild(month.root);
row.classList.toggle('justify-between', rowWidth >= width);
row.style.marginBottom = `${ITEM_SPACING}px`;
for (const itemEl of itemEls) {
const index = parseInt(itemEl.getAttribute('data-index'), 10);
const item = showcaseItems[index];
const aspect = item.width / item.height;
const baseWidth = (aspect * ROW_HEIGHT) * scaleFactor;
const actualWidth = baseWidth - (totalSpacing / itemEls.length);
itemEl.style.width = `${actualWidth}px`;
itemEl.style.height = `${scaleFactor * ROW_HEIGHT}px`;
itemEl.style.marginRight = `${ITEM_SPACING}px`;
row.appendChild(itemEl);
}
container.appendChild(row);
}
for (const monthEls of itemElementsByMonth) {
const month = monthTemplate();
const firstDate = new Date(parseFloat(monthEls[0].getAttribute('data-date')) * 1000);
month.dateHeader.textContent = firstDate.toLocaleDateString([], { month: 'long', year: 'numeric' });
let rowItemEls = [];
let rowWidth = 0;
for (const itemEl of monthEls) {
const index = parseInt(itemEl.getAttribute('data-index'), 10);
const item = showcaseItems[index];
const aspect = item.width / item.height;
rowWidth += aspect * ROW_HEIGHT;
rowItemEls.push(itemEl);
if (rowWidth > width) {
addRow(rowItemEls, rowWidth, month.itemsContainer);
rowItemEls = [];
rowWidth = 0;
}
}
addRow(rowItemEls, rowWidth, month.itemsContainer);
showcaseContainer.appendChild(month.root);
}
} }
function tryLoadImages() {
const OFFSCREEN_THRESHOLD = 0;
const months = document.querySelectorAll('.month-container');
for (const month of months) {
const rect = month.getBoundingClientRect();
const offscreen = (
rect.bottom < -OFFSCREEN_THRESHOLD
|| rect.top > window.innerHeight + OFFSCREEN_THRESHOLD
);
if (!offscreen) {
const items = month.querySelectorAll('.showcase-item');
for (const item of items) {
const i = parseInt(item.getAttribute('data-index'), 10);
addThumbnailFuncs[i]();
}
}
}
}
layout();
layout(); // scrollbars are fun!!
tryLoadImages();
window.addEventListener('resize', () => {
layout();
tryLoadImages();
});
window.addEventListener('scroll', () => {
tryLoadImages();
});
</script> </script>
{{ end }} {{ end }}

View File

@ -263,6 +263,7 @@ type TimelineItem struct {
PreviewMedia TimelineItemMedia PreviewMedia TimelineItemMedia
EmbedMedia []TimelineItemMedia EmbedMedia []TimelineItemMedia
SmallInfo bool
CanShowcase bool // whether this snippet can be shown in a showcase gallery CanShowcase bool // whether this snippet can be shown in a showcase gallery
} }

View File

@ -26,19 +26,24 @@ type FeedData struct {
MarkAllReadUrl string MarkAllReadUrl string
} }
func Feed(c *RequestContext) ResponseData { const feedPostsPerPage = 30
const postsPerPage = 30
var feedThreadTypes = []models.ThreadType{
models.ThreadTypeForumPost,
models.ThreadTypeProjectBlogPost,
}
func Feed(c *RequestContext) ResponseData {
numPosts, err := CountPosts(c.Context(), c.Conn, c.CurrentUser, PostsQuery{ numPosts, err := CountPosts(c.Context(), c.Conn, c.CurrentUser, PostsQuery{
ThreadTypes: []models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectBlogPost}, ThreadTypes: feedThreadTypes,
}) })
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err) return c.ErrorResponse(http.StatusInternalServerError, err)
} }
numPages := int(math.Ceil(float64(numPosts) / postsPerPage)) numPages := int(math.Ceil(float64(numPosts) / feedPostsPerPage))
page, numPages, ok := getPageInfo(c.PathParams["page"], numPosts, postsPerPage) page, numPages, ok := getPageInfo(c.PathParams["page"], numPosts, feedPostsPerPage)
if !ok { if !ok {
return c.Redirect(hmnurl.BuildFeed(), http.StatusSeeOther) return c.Redirect(hmnurl.BuildFeed(), http.StatusSeeOther)
} }
@ -53,7 +58,7 @@ func Feed(c *RequestContext) ResponseData {
PreviousUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page-1, numPages)), PreviousUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page-1, numPages)),
} }
posts, err := fetchAllPosts(c, (page-1)*postsPerPage, postsPerPage) posts, err := fetchAllPosts(c, (page-1)*feedPostsPerPage, feedPostsPerPage)
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
} }
@ -272,7 +277,7 @@ func AtomFeed(c *RequestContext) ResponseData {
func fetchAllPosts(c *RequestContext, offset int, limit int) ([]templates.PostListItem, error) { func fetchAllPosts(c *RequestContext, offset int, limit int) ([]templates.PostListItem, error) {
postsAndStuff, err := FetchPosts(c.Context(), c.Conn, c.CurrentUser, PostsQuery{ postsAndStuff, err := FetchPosts(c.Context(), c.Conn, c.CurrentUser, PostsQuery{
ThreadTypes: []models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectBlogPost}, ThreadTypes: feedThreadTypes,
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
SortDescending: true, SortDescending: true,

View File

@ -1,205 +1,88 @@
package website package website
import ( import (
"fmt"
"html/template" "html/template"
"math"
"net/http" "net/http"
"time"
"git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmnurl" "git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates" "git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils"
) )
type LandingTemplateData struct { type LandingTemplateData struct {
templates.BaseData templates.BaseData
NewsPost LandingPageFeaturedPost NewsPost *templates.TimelineItem
PostColumns [][]LandingPageProject TimelineItems []templates.TimelineItem
Pagination templates.Pagination
ShowcaseTimelineJson string ShowcaseTimelineJson string
FeedUrl string FeedUrl string
PodcastUrl string PodcastUrl string
StreamsUrl string StreamsUrl string
IRCUrl string IRCUrl string
DiscordUrl string DiscordUrl string
ShowUrl string ShowUrl string
ShowcaseUrl string ShowcaseUrl string
AtomFeedUrl string
MarkAllReadUrl string
WheelJamUrl string WheelJamUrl string
} }
type LandingPageProject struct {
Project templates.Project
FeaturedPost *LandingPageFeaturedPost
Posts []templates.PostListItem
ForumsUrl string
}
type LandingPageFeaturedPost struct {
Title string
Url string
User templates.User
Date time.Time
Unread bool
Content template.HTML
}
func Index(c *RequestContext) ResponseData { func Index(c *RequestContext) ResponseData {
const maxPosts = 5
const numProjectsToGet = 7
c.Perf.StartBlock("SQL", "Fetch projects")
iterProjects, err := db.Query(c.Context(), c.Conn, models.Project{},
`
SELECT $columns
FROM handmade_project
WHERE
(flags = 0 AND lifecycle = ANY($1))
OR id = $2
ORDER BY all_last_updated DESC
LIMIT $3
`,
models.VisibleProjectLifecycles,
models.HMNProjectID,
numProjectsToGet*2, // hedge your bets against projects that don't have any content
)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get projects for home page"))
}
defer iterProjects.Close()
var pageProjects []LandingPageProject
allProjects := iterProjects.ToSlice()
c.Perf.EndBlock()
c.Perf.StartBlock("SQL", "Fetch subforum tree") c.Perf.StartBlock("SQL", "Fetch subforum tree")
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn) subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree) lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
c.Perf.EndBlock() c.Perf.EndBlock()
c.Perf.StartBlock("LANDING", "Process projects") var timelineItems []templates.TimelineItem
for _, projRow := range allProjects {
proj := projRow.(*models.Project)
c.Perf.StartBlock("SQL", fmt.Sprintf("Fetch posts for %s", proj.Name)) numPosts, err := CountPosts(c.Context(), c.Conn, c.CurrentUser, PostsQuery{
projectPosts, err := FetchPosts(c.Context(), c.Conn, c.CurrentUser, PostsQuery{ ThreadTypes: feedThreadTypes,
ProjectIDs: []int{proj.ID}, })
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost, models.ThreadTypeForumPost}, if err != nil {
Limit: maxPosts, return c.ErrorResponse(http.StatusInternalServerError, err)
SortDescending: true,
})
c.Perf.EndBlock()
if err != nil {
c.Logger.Error().Err(err).Msg("failed to fetch project posts")
continue
}
forumsUrl := ""
if proj.ForumID != nil {
forumsUrl = hmnurl.BuildForum(proj.Slug, lineageBuilder.GetSubforumLineageSlugs(*proj.ForumID), 1)
} else {
c.Logger.Error().Int("ProjectID", proj.ID).Str("ProjectName", proj.Name).Msg("Project fetched by landing page but it doesn't have forums")
}
landingPageProject := LandingPageProject{
Project: templates.ProjectToTemplate(proj, c.Theme),
ForumsUrl: forumsUrl,
}
for _, projectPost := range projectPosts {
featurable := (!proj.IsHMN() &&
projectPost.Post.ThreadType == models.ThreadTypeProjectBlogPost &&
projectPost.Thread.FirstID == projectPost.Post.ID &&
landingPageProject.FeaturedPost == nil)
if featurable {
type featuredContentResult struct {
Content string `db:"ver.text_parsed"`
}
c.Perf.StartBlock("SQL", "Fetch featured post content")
contentResult, err := db.QueryOne(c.Context(), c.Conn, featuredContentResult{}, `
SELECT $columns
FROM
handmade_post AS post
JOIN handmade_postversion AS ver ON post.current_id = ver.id
WHERE
post.id = $1
`, projectPost.Post.ID)
c.Perf.EndBlock()
if err != nil {
c.Logger.Error().Err(err).Msg("failed to fetch featured post content")
continue
}
content := contentResult.(*featuredContentResult).Content
landingPageProject.FeaturedPost = &LandingPageFeaturedPost{
Title: projectPost.Thread.Title,
Url: hmnurl.BuildBlogThread(proj.Slug, projectPost.Thread.ID, projectPost.Thread.Title),
User: templates.UserToTemplate(projectPost.Author, c.Theme),
Date: projectPost.Post.PostDate,
Unread: projectPost.Unread,
Content: template.HTML(content),
}
} else {
landingPageProject.Posts = append(
landingPageProject.Posts,
MakePostListItem(
lineageBuilder,
proj,
&projectPost.Thread,
&projectPost.Post,
projectPost.Author,
projectPost.Unread,
false,
c.Theme,
),
)
}
}
if len(projectPosts) > 0 {
pageProjects = append(pageProjects, landingPageProject)
}
if len(pageProjects) >= numProjectsToGet {
break
}
} }
c.Perf.EndBlock()
/* numPages := int(math.Ceil(float64(numPosts) / feedPostsPerPage))
Columns are filled by placing projects into the least full column.
The fill array tracks the estimated sizes.
This is all hardcoded for two columns; deal with it. page, numPages, ok := getPageInfo("1", numPosts, feedPostsPerPage)
*/ if !ok {
cols := [][]LandingPageProject{nil, nil} return c.Redirect(hmnurl.BuildFeed(), http.StatusSeeOther)
fill := []int{4, 0} }
featuredIndex := []int{0, 0}
for _, pageProject := range pageProjects {
leastFullColumnIndex := indexOfSmallestInt(fill)
numNewPosts := len(pageProject.Posts) pagination := templates.Pagination{
if numNewPosts > maxPosts { Current: page,
numNewPosts = maxPosts Total: numPages,
}
FirstUrl: hmnurl.BuildFeed(),
fill[leastFullColumnIndex] += numNewPosts LastUrl: hmnurl.BuildFeedWithPage(numPages),
NextUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page+1, numPages)),
if pageProject.FeaturedPost != nil { PreviousUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page-1, numPages)),
fill[leastFullColumnIndex] += 2 // featured posts add more to height }
// projects with featured posts go at the top of the column // This is essentially an alternate for feed page 1.
cols[leastFullColumnIndex] = append(cols[leastFullColumnIndex], pageProject) posts, err := FetchPosts(c.Context(), c.Conn, c.CurrentUser, PostsQuery{
featuredIndex[leastFullColumnIndex] += 1 ThreadTypes: feedThreadTypes,
} else { Limit: feedPostsPerPage,
cols[leastFullColumnIndex] = append(cols[leastFullColumnIndex], pageProject) SortDescending: true,
})
if err != nil {
c.Logger.Warn().Err(err).Msg("failed to fetch latest posts")
}
for _, p := range posts {
item := PostToTimelineItem(lineageBuilder, &p.Post, &p.Thread, &p.Project, p.Author, c.Theme)
if p.Thread.Type == models.ThreadTypeProjectBlogPost && p.Post.ID == p.Thread.FirstID {
// blog post
item.Description = template.HTML(p.CurrentVersion.TextParsed)
item.TruncateDescription = true
} }
timelineItems = append(timelineItems, item)
} }
c.Perf.StartBlock("SQL", "Get news") c.Perf.StartBlock("SQL", "Get news")
@ -211,9 +94,14 @@ func Index(c *RequestContext) ResponseData {
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post"))
} }
var newsThread ThreadAndStuff var newsPostItem *templates.TimelineItem
if len(newsThreads) > 0 { if len(newsThreads) > 0 {
newsThread = newsThreads[0] t := newsThreads[0]
item := PostToTimelineItem(lineageBuilder, &t.FirstPost, &t.Thread, &t.Project, t.FirstPostAuthor, c.Theme)
item.TypeTitle = ""
item.Description = template.HTML(t.FirstPostCurrentVersion.TextParsed)
item.TruncateDescription = true
newsPostItem = &item
} }
c.Perf.EndBlock() c.Perf.EndBlock()
@ -235,7 +123,7 @@ func Index(c *RequestContext) ResponseData {
WHERE WHERE
NOT snippet.is_jam NOT snippet.is_jam
ORDER BY snippet.when DESC ORDER BY snippet.when DESC
LIMIT 20 LIMIT 40
`, `,
) )
if err != nil { if err != nil {
@ -265,25 +153,23 @@ func Index(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
err = res.WriteTemplate("landing.html", LandingTemplateData{ err = res.WriteTemplate("landing.html", LandingTemplateData{
BaseData: baseData, BaseData: baseData,
FeedUrl: hmnurl.BuildFeed(),
PodcastUrl: hmnurl.BuildPodcast(models.HMNProjectSlug), NewsPost: newsPostItem,
StreamsUrl: hmnurl.BuildStreams(), TimelineItems: timelineItems,
IRCUrl: hmnurl.BuildBlogThread(models.HMNProjectSlug, 1138, "[Tutorial] Handmade Network IRC"), Pagination: pagination,
DiscordUrl: "https://discord.gg/hxWxDee",
ShowUrl: "https://handmadedev.show/",
ShowcaseUrl: hmnurl.BuildShowcase(),
NewsPost: LandingPageFeaturedPost{
Title: newsThread.Thread.Title,
Url: hmnurl.BuildBlogThread(models.HMNProjectSlug, newsThread.Thread.ID, newsThread.Thread.Title),
User: templates.UserToTemplate(newsThread.FirstPostAuthor, c.Theme),
Date: newsThread.FirstPost.PostDate,
Unread: true,
Content: template.HTML(newsThread.FirstPostCurrentVersion.TextParsed),
},
PostColumns: cols,
ShowcaseTimelineJson: showcaseJson, ShowcaseTimelineJson: showcaseJson,
FeedUrl: hmnurl.BuildFeed(),
PodcastUrl: hmnurl.BuildPodcast(models.HMNProjectSlug),
StreamsUrl: hmnurl.BuildStreams(),
IRCUrl: hmnurl.BuildBlogThread(models.HMNProjectSlug, 1138, "[Tutorial] Handmade Network IRC"),
DiscordUrl: "https://discord.gg/hxWxDee",
ShowUrl: "https://handmadedev.show/",
ShowcaseUrl: hmnurl.BuildShowcase(),
AtomFeedUrl: hmnurl.BuildAtomFeed(),
MarkAllReadUrl: hmnurl.BuildForumMarkRead(models.HMNProjectSlug, 0),
WheelJamUrl: hmnurl.BuildJamIndex(), WheelJamUrl: hmnurl.BuildJamIndex(),
}, c.Perf) }, c.Perf)
if err != nil { if err != nil {
@ -292,17 +178,3 @@ func Index(c *RequestContext) ResponseData {
return res return res
} }
func indexOfSmallestInt(s []int) int {
result := 0
min := s[result]
for i, val := range s {
if val < min {
result = i
min = val
}
}
return result
}

View File

@ -98,6 +98,10 @@ func SnippetToTimelineItem(
} }
} }
if len(item.EmbedMedia) > 0 && (item.EmbedMedia[0].Width == 0 || item.EmbedMedia[0].Height == 0) {
item.CanShowcase = false
}
if discordMessage != nil { if discordMessage != nil {
item.DiscordMessageUrl = discordMessage.Url item.DiscordMessageUrl = discordMessage.Url
} }

View File

@ -177,13 +177,15 @@ func UserProfile(c *RequestContext) ResponseData {
for _, snippetRow := range snippetQuerySlice { for _, snippetRow := range snippetQuerySlice {
snippetData := snippetRow.(*snippetQuery) snippetData := snippetRow.(*snippetQuery)
timelineItems = append(timelineItems, SnippetToTimelineItem( item := SnippetToTimelineItem(
&snippetData.Snippet, &snippetData.Snippet,
snippetData.Asset, snippetData.Asset,
snippetData.DiscordMessage, snippetData.DiscordMessage,
profileUser, profileUser,
c.Theme, c.Theme,
)) )
item.SmallInfo = true
timelineItems = append(timelineItems, item)
} }
c.Perf.StartBlock("PROFILE", "Sort timeline") c.Perf.StartBlock("PROFILE", "Sort timeline")