Showcase page
This commit is contained in:
parent
77273cdb33
commit
8aa4554934
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 47.971 47.971">
|
||||||
|
<g>
|
||||||
|
<path d="M28.228,23.986L47.092,5.122c1.172-1.171,1.172-3.071,0-4.242c-1.172-1.172-3.07-1.172-4.242,0L23.986,19.744L5.121,0.88
|
||||||
|
c-1.172-1.172-3.07-1.172-4.242,0c-1.172,1.171-1.172,3.071,0,4.242l18.865,18.864L0.879,42.85c-1.172,1.171-1.172,3.071,0,4.242
|
||||||
|
C1.465,47.677,2.233,47.97,3,47.97s1.535-0.293,2.121-0.879l18.865-18.864L42.85,47.091c0.586,0.586,1.354,0.879,2.121,0.879
|
||||||
|
s1.535-0.293,2.121-0.879c1.172-1.171,1.172-3.071,0-4.242L28.228,23.986z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 776 B |
|
@ -0,0 +1,124 @@
|
||||||
|
const TimelineTypes = {
|
||||||
|
UNKNOWN: 0,
|
||||||
|
FORUM_THREAD: 1,
|
||||||
|
FORUM_REPLY: 2,
|
||||||
|
BLOG_POST: 3,
|
||||||
|
BLOG_COMMENT: 4,
|
||||||
|
WIKI_CREATE: 5,
|
||||||
|
WIKI_EDIT: 6,
|
||||||
|
WIKI_TALK: 7,
|
||||||
|
LIBRARY_COMMENT: 8,
|
||||||
|
SNIPPET_IMAGE: 9,
|
||||||
|
SNIPPET_VIDEO: 10,
|
||||||
|
SNIPPET_AUDIO: 11,
|
||||||
|
SNIPPET_YOUTUBE: 12
|
||||||
|
};
|
||||||
|
|
||||||
|
const showcaseItemTemplate = makeTemplateCloner("showcase_item");
|
||||||
|
const modalTemplate = makeTemplateCloner("timeline_modal");
|
||||||
|
|
||||||
|
function showcaseTimestamp(rawDate) {
|
||||||
|
const date = new Date(rawDate*1000);
|
||||||
|
return date.toLocaleDateString([], { 'dateStyle': 'long' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function doOnce(f) {
|
||||||
|
let did = false;
|
||||||
|
return () => {
|
||||||
|
if (!did) {
|
||||||
|
f();
|
||||||
|
did = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeShowcaseItem(timelineItem) {
|
||||||
|
const timestamp = showcaseTimestamp(timelineItem.date);
|
||||||
|
|
||||||
|
const itemEl = showcaseItemTemplate();
|
||||||
|
itemEl.avatar.style.backgroundImage = `url('${timelineItem.owner_avatar}')`;
|
||||||
|
itemEl.username.textContent = timelineItem.owner_name;
|
||||||
|
itemEl.when.textContent = timestamp;
|
||||||
|
|
||||||
|
let addThumbnailFunc = () => {};
|
||||||
|
let createModalContentFunc = () => {};
|
||||||
|
|
||||||
|
switch (timelineItem.type) {
|
||||||
|
case TimelineTypes.SNIPPET_IMAGE:
|
||||||
|
addThumbnailFunc = () => {
|
||||||
|
itemEl.thumbnail.style.backgroundImage = `url('${timelineItem.asset_url}')`;
|
||||||
|
};
|
||||||
|
|
||||||
|
createModalContentFunc = () => {
|
||||||
|
const modalImage = document.createElement('img');
|
||||||
|
modalImage.src = timelineItem.asset_url;
|
||||||
|
modalImage.classList.add('mw-100', 'mh-60vh');
|
||||||
|
return modalImage;
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
case TimelineTypes.SNIPPET_VIDEO:
|
||||||
|
addThumbnailFunc = () => {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = timelineItem.asset_url;
|
||||||
|
video.controls = false;
|
||||||
|
video.classList.add('h-100');
|
||||||
|
video.preload = 'metadata';
|
||||||
|
itemEl.thumbnail.appendChild(video);
|
||||||
|
};
|
||||||
|
|
||||||
|
createModalContentFunc = () => {
|
||||||
|
const modalVideo = document.createElement('video');
|
||||||
|
modalVideo.src = timelineItem.asset_url;
|
||||||
|
modalVideo.controls = true;
|
||||||
|
modalVideo.preload = 'metadata';
|
||||||
|
modalVideo.classList.add('mw-100', 'mh-60vh');
|
||||||
|
return modalVideo;
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
case TimelineTypes.SNIPPET_AUDIO:
|
||||||
|
createModalContentFunc = () => {
|
||||||
|
const modalAudio = document.createElement('audio');
|
||||||
|
modalAudio.src = timelineItem.asset_url;
|
||||||
|
modalAudio.controls = true;
|
||||||
|
modalAudio.preload = 'metadata';
|
||||||
|
modalAudio.classList.add('w-70');
|
||||||
|
return modalAudio;
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
// TODO(ben): Other snippet types?
|
||||||
|
}
|
||||||
|
|
||||||
|
let modalEl = null;
|
||||||
|
itemEl.container.addEventListener('click', function() {
|
||||||
|
if (!modalEl) {
|
||||||
|
modalEl = modalTemplate();
|
||||||
|
modalEl.description.innerHTML = timelineItem.description;
|
||||||
|
modalEl.asset_container.appendChild(createModalContentFunc());
|
||||||
|
|
||||||
|
modalEl.avatar.src = timelineItem.owner_avatar;
|
||||||
|
modalEl.userLink.textContent = timelineItem.owner_name;
|
||||||
|
modalEl.userLink.href = timelineItem.owner_url;
|
||||||
|
modalEl.date.textContent = timestamp;
|
||||||
|
modalEl.date.setAttribute("href", timelineItem.snippet_url);
|
||||||
|
|
||||||
|
modalEl.discord_link.href = timelineItem.discord_message_url;
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
modalEl.overlay.remove();
|
||||||
|
}
|
||||||
|
modalEl.overlay.addEventListener('click', close);
|
||||||
|
modalEl.close.addEventListener('click', close);
|
||||||
|
modalEl.container.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(modalEl.overlay);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [itemEl, doOnce(addThumbnailFunc)];
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
var domLookupCache = {};
|
||||||
|
var templatePathCache = {};
|
||||||
|
|
||||||
|
function getTemplateEl(id) {
|
||||||
|
if (!domLookupCache[id]) {
|
||||||
|
domLookupCache[id] = document.getElementById(id);
|
||||||
|
}
|
||||||
|
return domLookupCache[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectElements(paths, rootElement) {
|
||||||
|
var result = {};
|
||||||
|
for (var i = 0; i < paths.length; ++i) {
|
||||||
|
var path = paths[i];
|
||||||
|
var current = rootElement;
|
||||||
|
for (var j = 0; j < path[1].length; ++j) {
|
||||||
|
current = current.children[path[1][j]];
|
||||||
|
}
|
||||||
|
result[path[0]] = current;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTemplatePaths(id, rootElement) {
|
||||||
|
if (!templatePathCache[id]) {
|
||||||
|
var paths = [];
|
||||||
|
paths.push(["root", []]);
|
||||||
|
|
||||||
|
function descend(path, el) {
|
||||||
|
for (var i = 0; i < el.children.length; ++i) {
|
||||||
|
var child = el.children[i];
|
||||||
|
var childPath = path.concat([i]);
|
||||||
|
var tmplName = child.getAttribute("data-tmpl");
|
||||||
|
if (tmplName) {
|
||||||
|
paths.push([tmplName, childPath]);
|
||||||
|
}
|
||||||
|
if (child.children.length > 0) {
|
||||||
|
descend(childPath, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
descend([], rootElement);
|
||||||
|
templatePathCache[id] = paths;
|
||||||
|
}
|
||||||
|
return templatePathCache[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTemplateCloner(id) {
|
||||||
|
return function() {
|
||||||
|
var templateEl = getTemplateEl(id);
|
||||||
|
if (templateEl === null) {
|
||||||
|
throw new Error(`Couldn\'t find template with ID '${id}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = templateEl.content.cloneNode(true);
|
||||||
|
var paths = getTemplatePaths(id, root);
|
||||||
|
var result = collectElements(paths, root);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyElement(el) {
|
||||||
|
var newEl = el.cloneNode(false);
|
||||||
|
el.parentElement.insertBefore(newEl, el);
|
||||||
|
el.parentElement.removeChild(el);
|
||||||
|
return newEl;
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"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"
|
||||||
|
@ -223,6 +225,72 @@ func LinkToTemplate(link *models.Link) Link {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TimelineItemsToJSON(items []TimelineItem) string {
|
||||||
|
// NOTE(asaf): As of 2021-06-22: This only serializes the data necessary for snippet showcase.
|
||||||
|
builder := strings.Builder{}
|
||||||
|
builder.WriteRune('[')
|
||||||
|
for i, item := range items {
|
||||||
|
if i > 0 {
|
||||||
|
builder.WriteRune(',')
|
||||||
|
}
|
||||||
|
builder.WriteRune('{')
|
||||||
|
|
||||||
|
builder.WriteString(`"type":`)
|
||||||
|
builder.WriteString(strconv.Itoa(int(item.Type)))
|
||||||
|
builder.WriteRune(',')
|
||||||
|
|
||||||
|
builder.WriteString(`"date":`)
|
||||||
|
builder.WriteString(strconv.FormatInt(item.Date.UTC().Unix(), 10))
|
||||||
|
builder.WriteRune(',')
|
||||||
|
|
||||||
|
builder.WriteString(`"description":"`)
|
||||||
|
jsonString := string(item.Description)
|
||||||
|
jsonString = strings.ReplaceAll(jsonString, `\`, `\\`)
|
||||||
|
jsonString = strings.ReplaceAll(jsonString, `"`, `\"`)
|
||||||
|
jsonString = strings.ReplaceAll(jsonString, "\n", "\\n")
|
||||||
|
jsonString = strings.ReplaceAll(jsonString, "\r", "\\r")
|
||||||
|
jsonString = strings.ReplaceAll(jsonString, "\t", "\\t")
|
||||||
|
builder.WriteString(jsonString)
|
||||||
|
builder.WriteString(`",`)
|
||||||
|
|
||||||
|
builder.WriteString(`"owner_name":"`)
|
||||||
|
builder.WriteString(item.OwnerName)
|
||||||
|
builder.WriteString(`",`)
|
||||||
|
|
||||||
|
builder.WriteString(`"owner_avatar":"`)
|
||||||
|
builder.WriteString(item.OwnerAvatarUrl)
|
||||||
|
builder.WriteString(`",`)
|
||||||
|
|
||||||
|
builder.WriteString(`"owner_url":"`)
|
||||||
|
builder.WriteString(item.OwnerUrl)
|
||||||
|
builder.WriteString(`",`)
|
||||||
|
|
||||||
|
builder.WriteString(`"snippet_url":"`)
|
||||||
|
builder.WriteString(item.Url)
|
||||||
|
builder.WriteString(`",`)
|
||||||
|
|
||||||
|
builder.WriteString(`"width":`)
|
||||||
|
builder.WriteString(strconv.Itoa(item.Width))
|
||||||
|
builder.WriteRune(',')
|
||||||
|
|
||||||
|
builder.WriteString(`"height":`)
|
||||||
|
builder.WriteString(strconv.Itoa(item.Height))
|
||||||
|
builder.WriteRune(',')
|
||||||
|
|
||||||
|
builder.WriteString(`"asset_url":"`)
|
||||||
|
builder.WriteString(item.AssetUrl)
|
||||||
|
builder.WriteString(`",`)
|
||||||
|
|
||||||
|
builder.WriteString(`"discord_message_url":"`)
|
||||||
|
builder.WriteString(item.DiscordMessageUrl)
|
||||||
|
builder.WriteString(`"`)
|
||||||
|
|
||||||
|
builder.WriteRune('}')
|
||||||
|
}
|
||||||
|
builder.WriteRune(']')
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
func maybeString(s *string) string {
|
func maybeString(s *string) string {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<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="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="gradient relative">
|
||||||
|
<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 class="flex-grow-1 flex flex-column pl1">
|
||||||
|
<div data-tmpl="username">Unknown User</div>
|
||||||
|
<div data-tmpl="when" class="i f8">Unknown Time</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="timeline_modal">
|
||||||
|
<div data-tmpl="overlay" class="timeline-modal fixed absolute--fill bg-black-80 z-999 flex flex-column justify-center items-center">
|
||||||
|
<div data-tmpl="container" class="container timeline-item relative flex-shrink-0 shadow-1 flex flex-column items-stretch w-100 mh0 mh3-ns br2-ns overflow-hidden">
|
||||||
|
<div data-tmpl="asset_container" class="bg-dark-gray flex justify-center"></div>
|
||||||
|
<div class="bg--content pa3 overflow-y-auto">
|
||||||
|
<div class="timeline-user-info mb2 flex items-center">
|
||||||
|
<img class="avatar-icon lite mr2" data-tmpl="avatar"/>
|
||||||
|
<a class="user" data-tmpl="userLink"></a>
|
||||||
|
<a data-tmpl="date" class="datetime tr" style="flex: 1 1 auto;"></a>
|
||||||
|
</div>
|
||||||
|
<div data-tmpl="description">
|
||||||
|
Unknown description
|
||||||
|
</div>
|
||||||
|
<div class="i f7 pt2">
|
||||||
|
<a data-tmpl="discord_link" target="_blank">View original message on Discord</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-tmpl="close" class="absolute right-0 top-0 w2 h2 flex justify-center items-center bg-black-80 white br-100 ma1 pointer svgicon svgicon-nofix"><img src="{{ static "close.svg" }}" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,171 @@
|
||||||
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
|
{{ define "extrahead" }}
|
||||||
|
<script src="{{ static "js/templates.js" }}"></script>
|
||||||
|
<script src="{{ static "js/showcase.js" }}"></script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="content-block">
|
||||||
|
<div class="ph2 ph0-ns pb4">
|
||||||
|
<div class="optionbar">
|
||||||
|
<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="{{ .ShowcaseAtomFeedUrl }}"><span class="icon big">4</span> Showcase Feed</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="showcase-container" class="mh2 mh0-ns pb4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ template "showcase_templates.html" }}
|
||||||
|
|
||||||
|
<template id="showcase-month">
|
||||||
|
<h3 data-tmpl="dateHeader" class="mt3 f4 fw5">Unknown Date</h3>
|
||||||
|
<div data-tmpl="itemsContainer" class="month-container"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ROW_HEIGHT = 300;
|
||||||
|
const ITEM_SPACING = 4;
|
||||||
|
|
||||||
|
const monthTemplate = makeTemplateCloner('showcase-month');
|
||||||
|
|
||||||
|
const showcaseItems = JSON.parse("{{ .ShowcaseItems }}");
|
||||||
|
const addThumbnailFuncs = new Array(showcaseItems.length);
|
||||||
|
|
||||||
|
let showcaseContainer = document.querySelector('#showcase-container');
|
||||||
|
|
||||||
|
const itemElementsByMonth = []; // array of arrays
|
||||||
|
let currentMonthElements = [];
|
||||||
|
let currentMonth = null;
|
||||||
|
let currentYear = null;
|
||||||
|
for (let i = 0; i < showcaseItems.length; i++) {
|
||||||
|
const item = showcaseItems[i];
|
||||||
|
const date = new Date(item.date * 1000); // TODO(asaf): Verify that this is still correct with our new JSON marshalling
|
||||||
|
|
||||||
|
if (date.getMonth() !== currentMonth || date.getFullYear() !== currentYear) {
|
||||||
|
if (currentMonthElements.length > 0) {
|
||||||
|
itemElementsByMonth.push(currentMonthElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMonthElements = [];
|
||||||
|
currentMonth = date.getMonth();
|
||||||
|
currentYear = date.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [itemEl, addThumbnail] = makeShowcaseItem(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) {
|
||||||
|
itemElementsByMonth.push(currentMonthElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
function layout() {
|
||||||
|
const width = showcaseContainer.getBoundingClientRect().width;
|
||||||
|
showcaseContainer = emptyElement(showcaseContainer);
|
||||||
|
|
||||||
|
function addRow(itemEls, rowWidth, container) {
|
||||||
|
const totalSpacing = ITEM_SPACING * (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 = `${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>
|
||||||
|
{{ end }}
|
|
@ -107,6 +107,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
// NOTE(asaf): HMN-only routes:
|
// NOTE(asaf): HMN-only routes:
|
||||||
mainRoutes.GET(hmnurl.RegexFeed, Feed)
|
mainRoutes.GET(hmnurl.RegexFeed, Feed)
|
||||||
mainRoutes.GET(hmnurl.RegexAtomFeed, AtomFeed)
|
mainRoutes.GET(hmnurl.RegexAtomFeed, AtomFeed)
|
||||||
|
mainRoutes.GET(hmnurl.RegexShowcase, Showcase)
|
||||||
mainRoutes.GET(hmnurl.RegexProjectIndex, ProjectIndex)
|
mainRoutes.GET(hmnurl.RegexProjectIndex, ProjectIndex)
|
||||||
mainRoutes.GET(hmnurl.RegexUserProfile, UserProfile)
|
mainRoutes.GET(hmnurl.RegexUserProfile, UserProfile)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
package website
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShowcaseData struct {
|
||||||
|
templates.BaseData
|
||||||
|
ShowcaseItems string // NOTE(asaf): JSON string
|
||||||
|
ShowcaseAtomFeedUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Showcase(c *RequestContext) ResponseData {
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch showcase snippets")
|
||||||
|
type snippetQuery struct {
|
||||||
|
Owner models.User `db:"owner"`
|
||||||
|
Snippet models.Snippet `db:"snippet"`
|
||||||
|
Asset *models.Asset `db:"asset"`
|
||||||
|
DiscordMessage *models.DiscordMessage `db:"discord_message"`
|
||||||
|
}
|
||||||
|
snippetQueryResult, err := db.Query(c.Context(), c.Conn, snippetQuery{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_snippet AS snippet
|
||||||
|
INNER JOIN auth_user AS owner ON owner.id = snippet.owner_id
|
||||||
|
LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
|
||||||
|
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
||||||
|
ORDER BY snippet.when DESC
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets"))
|
||||||
|
}
|
||||||
|
snippetQuerySlice := snippetQueryResult.ToSlice()
|
||||||
|
showcaseItems := make([]templates.TimelineItem, 0, len(snippetQuerySlice))
|
||||||
|
for _, s := range snippetQuerySlice {
|
||||||
|
row := s.(*snippetQuery)
|
||||||
|
timelineItem := SnippetToTimelineItem(&row.Snippet, row.Asset, row.DiscordMessage, &row.Owner, c.Theme)
|
||||||
|
if timelineItem.Type != templates.TimelineTypeSnippetYoutube {
|
||||||
|
showcaseItems = append(showcaseItems, timelineItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SHOWCASE", "Convert to json")
|
||||||
|
jsonItems := templates.TimelineItemsToJSON(showcaseItems)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
baseData := getBaseData(c)
|
||||||
|
baseData.Title = "Community Showcase"
|
||||||
|
var res ResponseData
|
||||||
|
err = res.WriteTemplate("showcase.html", ShowcaseData{
|
||||||
|
BaseData: baseData,
|
||||||
|
ShowcaseItems: jsonItems,
|
||||||
|
ShowcaseAtomFeedUrl: hmnurl.BuildAtomFeedForShowcase(),
|
||||||
|
}, c.Perf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
Loading…
Reference in New Issue