diff options
-rw-r--r-- | src/data/things/homepage-layout.js | 14 | ||||
-rw-r--r-- | src/data/things/validators.js | 10 | ||||
-rw-r--r-- | src/data/yaml.js | 1 | ||||
-rw-r--r-- | src/misc-templates.js | 43 | ||||
-rw-r--r-- | src/page/homepage.js | 45 | ||||
-rw-r--r-- | src/static/site2.css | 135 | ||||
-rwxr-xr-x | src/upd8.js | 7 | ||||
-rw-r--r-- | src/util/sugar.js | 14 |
8 files changed, 260 insertions, 9 deletions
diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 5948ff46..32b8cf2a 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -65,6 +65,7 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { isCountingNumber, isString, validateArrayItems, + validateFromConstants, }, } = opts) => ({ ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts), @@ -84,6 +85,19 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }, }, + displayStyle: { + flags: {update: true, expose: true}, + + update: { + validate: validateFromConstants('grid', 'montage'), + }, + + expose: { + transform: (displayStyle) => + displayStyle ?? 'grid', + }, + }, + sourceGroupByRef: Thing.common.singleReference(Group), sourceAlbumsByRef: Thing.common.referenceList(Album), diff --git a/src/data/things/validators.js b/src/data/things/validators.js index cc603d48..a0d473ba 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -162,6 +162,16 @@ export function validateInstanceOf(constructor) { return (object) => isInstance(object, constructor); } +export function validateFromConstants(...values) { + return (value) => { + if (!values.includes(value)) { + throw new TypeError(`Expected one of ${values.join(', ')}`); + } + + return true; + }; +} + // Wiki data (primitives & non-primitives) export function isColor(color) { diff --git a/src/data/yaml.js b/src/data/yaml.js index ae160d59..f967cee3 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -421,6 +421,7 @@ export function makeProcessHomepageLayoutRowDocument(rowClass, spec) { export const homepageLayoutRowTypeProcessMapping = { albums: makeProcessHomepageLayoutRowDocument(T.HomepageLayoutAlbumsRow, { propertyFieldMapping: { + displayStyle: 'Display Style', sourceGroupByRef: 'Group', countAlbumsFromGroup: 'Count', sourceAlbumsByRef: 'Albums', diff --git a/src/misc-templates.js b/src/misc-templates.js index 5dd96492..53a96059 100644 --- a/src/misc-templates.js +++ b/src/misc-templates.js @@ -6,6 +6,7 @@ import T from './data/things/index.js'; import { empty, + repeat, unique, } from './util/sugar.js'; @@ -639,6 +640,46 @@ function unbound_getFlashGridHTML({ }); } +// Montage reels + +function unbound_getMontageHTML({ + html, + img, + + items, + lazy = true, + + altFn = () => '', + linkFn = (x, {text}) => text, + srcFn, +}) { + return (x => x)(html.tag('div', {class: 'montage-container'}, + repeat(3, + html.tag('div', + { + class: 'montage-grid', + 'aria-hidden': 'true', + }, + items + .filter(item => srcFn(item)) + .filter(item => item.artTags.every(tag => !tag.isContentWarning)) + .map((item, i) => + html.tag('div', {class: 'montage-item'}, + linkFn(item, { + attributes: { + tabindex: '-1', + }, + text: + img({ + src: srcFn(item), + alt: altFn(item), + thumb: 'small', + lazy: typeof lazy === 'number' ? i >= lazy : lazy, + square: true, + }), + }))))))); +} + // Nav-bar links function unbound_generateInfoGalleryLinks(currentThing, isGallery, { @@ -837,6 +878,8 @@ export { unbound_getAlbumGridHTML as getAlbumGridHTML, unbound_getFlashGridHTML as getFlashGridHTML, + unbound_getMontageHTML as getMontageHTML, + unbound_generateInfoGalleryLinks as generateInfoGalleryLinks, unbound_generateNavigationLinks as generateNavigationLinks, diff --git a/src/page/homepage.js b/src/page/homepage.js index 105c402f..c592efa6 100644 --- a/src/page/homepage.js +++ b/src/page/homepage.js @@ -16,15 +16,17 @@ export function writeTargetless({wikiData}) { switch (type) { case 'albums': { + entry.displayStyle = row.displayStyle; + switch (row.sourceGroupByRef) { case 'new-releases': - entry.gridEntries = getNewReleases(row.countAlbumsFromGroup, {wikiData}); + entry.entries = getNewReleases(row.countAlbumsFromGroup, {wikiData}); break; case 'new-additions': - entry.gridEntries = getNewAdditions(row.countAlbumsFromGroup, {wikiData}); + entry.entries = getNewAdditions(row.countAlbumsFromGroup, {wikiData}); break; default: - entry.gridEntries = row.sourceGroup + entry.entries = row.sourceGroup ? row.sourceGroup.albums .slice() .reverse() @@ -35,7 +37,7 @@ export function writeTargetless({wikiData}) { } if (!empty(row.sourceAlbums)) { - entry.gridEntries.push(...row.sourceAlbums.map(album => ({item: album}))); + entry.entries.push(...row.sourceAlbums.map(album => ({item: album}))); } entry.actionLinks = row.actionLinks ?? []; @@ -46,12 +48,20 @@ export function writeTargetless({wikiData}) { return entry; }); + const transformActionLinks = (actionLinks, { + transformInline, + }) => + actionLinks?.map(transformInline) + .map(a => a.replace('<a', '<a class="box grid-item"')); + const page = { type: 'page', path: ['home'], page: ({ getAlbumGridHTML, + getAlbumCover, getLinkThemeString, + getMontageHTML, html, language, link, @@ -84,10 +94,11 @@ export function writeTargetless({wikiData}) { entry.name), entry.type === 'albums' && + entry.displayStyle === 'grid' && html.tag('div', {class: 'grid-listing'}, [ ...html.fragment( getAlbumGridHTML({ - entries: entry.gridEntries, + entries: entry.entries, lazy: i > 0, })), @@ -96,9 +107,27 @@ export function writeTargetless({wikiData}) { [html.onlyIfContent]: true, class: 'grid-actions', }, - entry.actionLinks?.map(action => - transformInline(action) - .replace('<a', '<a class="box grid-item"'))), + transformActionLinks(entry.actionLinks, { + transformInline, + })), + ]), + + ...html.fragment( + entry.type === 'albums' && + entry.displayStyle === 'montage' && [ + getMontageHTML({ + items: entry.entries.map(e => e.item), + lazy: i > 0, + srcFn: getAlbumCover, + linkFn: link.album, + }), + + entry.actionLinks.length && + html.tag('div', {class: 'grid-listing'}, + html.tag('div', {class: 'grid-actions'}, + transformActionLinks(entry.actionLinks, { + transformInline, + }))), ]), ]))), ]), diff --git a/src/static/site2.css b/src/static/site2.css index 1146b0df..b97d85c6 100644 --- a/src/static/site2.css +++ b/src/static/site2.css @@ -930,7 +930,7 @@ img { .grid-actions { display: flex; - flex-direction: column; + flex-direction: row; margin: 15px; align-self: center; } @@ -955,6 +955,139 @@ img { font-size: 0.9em; } +/* Montage */ + +.montage-container { + position: relative; + overflow: hidden; + margin: 20px 0 5px 0; + padding: 8px 0; +} + +.montage-container::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -20; + background-color: var(--dim-color); + filter: brightness(0.6); +} + +.montage-container::after { + content: ""; + pointer-events: none; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px solid var(--primary-color); + border-radius: 4px; + z-index: 40; + box-shadow: + inset 20px 2px 40px var(--shadow-color), + inset -20px -2px 40px var(--shadow-color); +} + +.montage-container:hover .montage-grid { + animation-play-state: running; +} + +.montage-grid:nth-child(2), +.montage-grid:nth-child(3) { + position: absolute; + top: 8px; + left: 0; + right: 0; +} + +.montage-grid:nth-child(2) { + animation-name: montage-marquee2; +} + +.montage-grid:nth-child(3) { + animation-name: montage-marquee3; +} + +@keyframes montage-marquee1 { + 0% { + transform: translateX(0%) translateX(-70px); + } + + 100% { + transform: translateX(-100%) translateX(-70px); + } +} + +@keyframes montage-marquee2 { + 0% { + transform: translateX(100%) translateX(-70px); + } + + 100% { + transform: translateX(0%) translateX(-70px); + } +} + +@keyframes montage-marquee3 { + 0% { + transform: translateX(200%) translateX(-70px); + } + + 100% { + transform: translateX(100%) translateX(-70px); + } +} + +.montage-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + grid-template-rows: repeat(2, auto); + grid-auto-flow: dense; + grid-auto-rows: 0; + overflow: hidden; + margin: auto; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + z-index: 1; + + transform: translateX(0); + animation: montage-marquee1 40s linear infinite; + animation-play-state: paused; + z-index: 5; + filter: brightness(0.6); +} + +.montage-item { + display: inline-block; + margin: 0; + flex: 1 1 150px; + padding: 3px; + border-radius: 10px; +} + +.montage-item .image-container { + border: none; + padding: 0; +} + +.montage-item img { + width: 100%; + height: 100%; + margin-top: auto; + margin-bottom: auto; + border-radius: 6px; +} + +.montage-item:hover { + filter: brightness(1.2); + background: var(--dim-color); +} + /* Squares */ .square { diff --git a/src/upd8.js b/src/upd8.js index 20b0d28b..3d5da125 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -89,6 +89,7 @@ import { getFlashGridHTML, getFooterLocalizationLinks, getGridHTML, + getMontageHTML, getRevealStringFromTags, getRevealStringFromWarnings, getThemeString as unbound_getThemeString, @@ -2488,6 +2489,12 @@ async function main() { getGridHTML: bound.getGridHTML, }); + bound.getMontageHTML = bindOpts(getMontageHTML, { + [bindOpts.bindIndex]: 0, + img, + html, + }) + bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, { to, }); diff --git a/src/util/sugar.js b/src/util/sugar.js index e8fdf932..0813c1d4 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -40,6 +40,20 @@ export function empty(arrayOrNull) { } } +// Repeats all the items of an array a number of times. +export function repeat(times, array) { + if (typeof array === 'string') return repeat(times, [array]); + if (empty(array)) return []; + if (times === 0) return []; + if (times === 1) return array.slice(); + + const out = []; + for (let n = 1; n <= times; n++) { + out.push(...array); + } + return out; +} + // Sums the values in an array, optionally taking a function which maps each // item to a number (handy for accessing a certain property on an array of like // objects). This also coalesces null values to zero, so if the mapping function |