Rework the home page
This commit is contained in:
parent
7d422cb533
commit
f8d5f9fce5
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(',')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 }}
|
||||||
— {{ timehtml (relativedate .Date) .Date }}
|
— {{ 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 }}
|
||||||
|
|
|
@ -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">✓</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 →
|
|
||||||
</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> — {{ 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 →</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> — {{ timehtml (relativedate .Date) .Date }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="goto">
|
|
||||||
<a href="{{ .Url }}">»</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue