« get me outta code hell

properly support 4-18 item carousels - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2022-12-28 18:25:51 -0400
committer(quasar) nebula <qznebula@protonmail.com>2022-12-28 18:25:51 -0400
commit0fbce8a83c33a17821716d662c86a95848c7e58a (patch)
tree1b8ec19521e8390cf5e6f70893b1d4c37decef93
parent4431905279d30f2f987cea1e40805ffdd7d61b5b (diff)
properly support 4-18 item carousels
-rw-r--r--src/data/things/group.js4
-rw-r--r--src/data/yaml.js2
-rw-r--r--src/misc-templates.js55
-rw-r--r--src/page/group.js8
-rw-r--r--src/static/site2.css28
5 files changed, 91 insertions, 6 deletions
diff --git a/src/data/things/group.js b/src/data/things/group.js
index 26fe9a55..111d6715 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -17,6 +17,8 @@ export class Group extends Thing {
 
     urls: Thing.common.urls(),
 
+    featuredAlbumsByRef: Thing.common.referenceList(Album),
+
     // Update only
 
     albumData: Thing.common.wikiData(Album),
@@ -24,6 +26,8 @@ export class Group extends Thing {
 
     // Expose only
 
+    featuredAlbums: Thing.common.dynamicThingsFromReferenceList('featuredAlbumsByRef', 'albumData', find.album),
+
     descriptionShort: {
       flags: {expose: true},
 
diff --git a/src/data/yaml.js b/src/data/yaml.js
index f967cee3..a5a0e798 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -356,6 +356,8 @@ export const processGroupDocument = makeProcessDocument(T.Group, {
     directory: 'Directory',
     description: 'Description',
     urls: 'URLs',
+
+    featuredAlbumsByRef: 'Featured Albums',
   },
 });
 
diff --git a/src/misc-templates.js b/src/misc-templates.js
index 7887dcbd..71c62883 100644
--- a/src/misc-templates.js
+++ b/src/misc-templates.js
@@ -642,6 +642,48 @@ function unbound_getFlashGridHTML({
 
 // Carousel reels
 
+// Layout constants:
+//
+// Carousels support fitting 4-18 items, with a few "dead" zones to watch out
+// for, namely when a multiple of 6, 5, or 4 columns would drop the last tiles.
+//
+// Carousels are limited to 1-3 rows and 4-6 columns.
+// Lower edge case: 1-3 items are treated as 4 items (with blank space).
+// Upper edge case: all items past 18 are dropped (treated as 18 items).
+//
+// This is all done through JS instead of CSS because it's just... ANNOYING...
+// to write a mapping like this in CSS lol.
+const carouselLayoutMap = [
+  // 0-3
+  null, null, null, null,
+
+  // 4-6
+  {rows: 1, columns: 4}, //  4: 1x4, drop 0
+  {rows: 1, columns: 5}, //  5: 1x5, drop 0
+  {rows: 1, columns: 6}, //  6: 1x6, drop 0
+
+  // 7-12
+  {rows: 1, columns: 6}, //  7: 1x6, drop 1
+  {rows: 2, columns: 4}, //  8: 2x4, drop 0
+  {rows: 2, columns: 4}, //  9: 2x4, drop 1
+  {rows: 2, columns: 5}, // 10: 2x5, drop 0
+  {rows: 2, columns: 5}, // 11: 2x5, drop 1
+  {rows: 2, columns: 6}, // 12: 2x6, drop 0
+
+  // 13-18
+  {rows: 2, columns: 6}, // 13: 2x6, drop 1
+  {rows: 2, columns: 6}, // 14: 2x6, drop 2
+  {rows: 3, columns: 5}, // 15: 3x5, drop 0
+  {rows: 3, columns: 5}, // 16: 3x5, drop 1
+  {rows: 3, columns: 5}, // 17: 3x5, drop 2
+  {rows: 3, columns: 6}, // 18: 3x6, drop 0
+];
+
+const minCarouselLayoutItems = carouselLayoutMap.findIndex(x => x !== null);
+const maxCarouselLayoutItems = carouselLayoutMap.length - 1;
+const shortestCarouselLayout = carouselLayoutMap[minCarouselLayoutItems];
+const longestCarouselLayout = carouselLayoutMap[maxCarouselLayoutItems];
+
 function unbound_getCarouselHTML({
   html,
   img,
@@ -653,11 +695,24 @@ function unbound_getCarouselHTML({
   linkFn = (x, {text}) => text,
   srcFn,
 }) {
+  if (empty(items)) {
+    return;
+  }
+
+  const {rows, columns} = (
+    items.length < minCarouselLayoutItems ? shortestCarouselLayout :
+    items.length > maxCarouselLayoutItems ? longestCarouselLayout :
+    carouselLayoutMap[items.length]);
+
+  items = items.slice(0, maxCarouselLayoutItems + 1);
+
   return (x => x)(html.tag('div', {class: 'carousel-container'},
     repeat(3,
       html.tag('div',
         {
           class: 'carousel-grid',
+          'data-carousel-rows': rows,
+          'data-carousel-columns': columns,
           'aria-hidden': 'true',
         },
         items
diff --git a/src/page/group.js b/src/page/group.js
index 429097f8..26230b45 100644
--- a/src/page/group.js
+++ b/src/page/group.js
@@ -129,7 +129,9 @@ export function write(group, {wikiData}) {
     page: ({
       generateInfoGalleryLinks,
       generateNavigationLinks,
+      getAlbumCover,
       getAlbumGridHTML,
+      getCarouselHTML,
       getLinkThemeString,
       getThemeString,
       html,
@@ -149,6 +151,12 @@ export function write(group, {wikiData}) {
               group: group.name,
             })),
 
+          getCarouselHTML({
+            items: group.featuredAlbums.slice(0, 12 + 1),
+            srcFn: getAlbumCover,
+            linkFn: link.album,
+          }),
+
           html.tag('p',
             {class: 'quick-info'},
             language.$('groupGalleryPage.infoLine', {
diff --git a/src/static/site2.css b/src/static/site2.css
index fe90d4de..86b815d2 100644
--- a/src/static/site2.css
+++ b/src/static/site2.css
@@ -943,17 +943,22 @@ img {
 }
 
 .carousel-grid {
+  --carousel-tile-min-width: 120px;
+  --carousel-row-count: 3;
+  --carousel-column-count: 6;
+
+  /* Thanks to: https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/ */
+  --carousel-gap-count: calc(var(--carousel-column-count) - 1);
+  --carousel-total-gap-width: calc(var(--carousel-gap-count) * 10px);
+  --carousel-calculated-tile-max-width: calc((100% - var(--carousel-total-gap-width)) / var(--carousel-column-count));
+
   display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
-  grid-template-rows: repeat(2, auto);
+  grid-template-columns: repeat(auto-fill, minmax(max(var(--carousel-tile-min-width), var(--carousel-calculated-tile-max-width)), 1fr));
+  grid-template-rows: repeat(var(--carousel-row-count), 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: carousel-marquee1 40s linear infinite;
@@ -961,6 +966,17 @@ img {
   z-index: 5;
 }
 
+html[data-url-key="localized.home"] .carousel-grid {
+  --carousel-tile-size: 140px;
+}
+
+.carousel-grid[data-carousel-rows="1"] { --carousel-row-count: 1; }
+.carousel-grid[data-carousel-rows="2"] { --carousel-row-count: 2; }
+.carousel-grid[data-carousel-rows="3"] { --carousel-row-count: 3; }
+.carousel-grid[data-carousel-columns="4"] { --carousel-column-count: 4; }
+.carousel-grid[data-carousel-columns="5"] { --carousel-column-count: 5; }
+.carousel-grid[data-carousel-columns="6"] { --carousel-column-count: 6; }
+
 .carousel-item {
   display: inline-block;
   margin: 0;