« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/data/things/homepage-layout.js14
-rw-r--r--src/data/things/validators.js10
-rw-r--r--src/data/yaml.js1
-rw-r--r--src/misc-templates.js43
-rw-r--r--src/page/homepage.js45
-rw-r--r--src/static/site2.css135
-rwxr-xr-xsrc/upd8.js7
-rw-r--r--src/util/sugar.js14
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