« 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/content/dependencies/generateExpandableGallerySection.js92
-rw-r--r--src/content/dependencies/generateGroupGalleryPage.js93
-rw-r--r--src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js37
-rw-r--r--src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js26
-rw-r--r--src/content/dependencies/generateGroupGalleryPageSeriesSection.js129
-rw-r--r--src/data/things/language.js43
-rw-r--r--src/static/css/site.css42
-rw-r--r--src/static/js/client/expandable-gallery-section.js77
-rw-r--r--src/static/js/client/index.js2
-rw-r--r--src/static/js/client/quick-description.js2
-rw-r--r--src/strings-default.yaml30
11 files changed, 544 insertions, 29 deletions
diff --git a/src/content/dependencies/generateExpandableGallerySection.js b/src/content/dependencies/generateExpandableGallerySection.js
new file mode 100644
index 00000000..122ca4b1
--- /dev/null
+++ b/src/content/dependencies/generateExpandableGallerySection.js
@@ -0,0 +1,92 @@
+export default {
+  contentDependencies: ['generateContentHeading'],
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation) => ({
+    contentHeading:
+      relation('generateContentHeading'),
+  }),
+
+  slots: {
+    title: {
+      type: 'html',
+      mutable: false,
+    },
+
+    contentAboveCut: {
+      type: 'html',
+      mutable: false,
+    },
+
+    contentBelowCut: {
+      type: 'html',
+      mutable: false,
+    },
+
+    caption: {
+      type: 'html',
+      mutable: false,
+    },
+
+    expandCue: {
+      type: 'html',
+      mutable: false,
+    },
+
+    collapseCue: {
+      type: 'html',
+      mutable: false,
+    },
+  },
+
+  generate: (relations, slots, {html, language}) =>
+    html.tag('section', {class: 'expandable-gallery-section'}, [
+      relations.contentHeading.slots({
+        tag: 'h2',
+        title: slots.title,
+      }),
+
+      html.tag('div', {class: 'section-content-above-cut'},
+        {[html.onlyIfContent]: true},
+
+        slots.contentAboveCut),
+
+      html.tag('div', {class: 'section-content-below-cut'},
+        {[html.onlyIfContent]: true},
+
+        !html.isBlank(slots.contentBelowCut) &&
+          {style: 'display: none'},
+
+        slots.contentBelowCut),
+
+      html.tag('div', {class: 'section-expando'},
+        {[html.onlyIfSiblings]: true},
+
+        html.tag('div', {class: 'section-expando-content'},
+          {[html.joinChildren]: html.tag('br')},
+
+          [
+            html.tag('span', {class: 'section-caption'},
+              slots.caption),
+
+            !html.isBlank(slots.contentBelowCut) &&
+              language.$('misc.coverGrid.expandCollapseCue', {
+                cue:
+                  html.tag('a', {class: 'section-expando-toggle'},
+                    {href: '#'},
+
+                    {[html.joinChildren]: ''},
+                    {[html.noEdgeWhitespace]: true},
+
+                    [
+                      html.tag('span', {class: 'section-expand-cue'},
+                        slots.expandCue),
+
+                      html.tag('span', {class: 'section-collapse-cue'},
+                        {style: 'display: none'},
+                        slots.collapseCue),
+                    ]),
+              }),
+          ])),
+    ]),
+};
diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js
index 453155cb..535a2bc9 100644
--- a/src/content/dependencies/generateGroupGalleryPage.js
+++ b/src/content/dependencies/generateGroupGalleryPage.js
@@ -4,10 +4,11 @@ import {filterItemsForCarousel, getTotalDuration} from '#wiki-data';
 export default {
   contentDependencies: [
     'generateCoverCarousel',
-    'generateGroupGalleryPageAlbumGrid',
+    'generateGroupGalleryPageAlbumsByDateView',
+    'generateGroupGalleryPageAlbumsBySeriesView',
     'generateGroupNavLinks',
     'generateGroupSecondaryNav',
-    'generateGroupSidebar',
+    'generateIntrapageDotSwitcher',
     'generatePageLayout',
     'generateQuickDescription',
     'image',
@@ -47,11 +48,6 @@ export default {
         ? relation('generateGroupSecondaryNav', group)
         : null),
 
-    sidebar:
-      (sprawl.enableGroupUI
-        ? relation('generateGroupSidebar', group)
-        : null),
-
     coverCarousel:
       relation('generateCoverCarousel'),
 
@@ -66,8 +62,14 @@ export default {
     quickDescription:
       relation('generateQuickDescription', group),
 
-    albumGrid:
-      relation('generateGroupGalleryPageAlbumGrid', query.allAlbums),
+    albumViewSwitcher:
+      relation('generateIntrapageDotSwitcher'),
+
+    albumsBySeriesView:
+      relation('generateGroupGalleryPageAlbumsBySeriesView', group),
+
+    albumsByDateView:
+      relation('generateGroupGalleryPageAlbumsByDateView', group),
   }),
 
   data: (query, _sprawl, group) => ({
@@ -125,23 +127,76 @@ export default {
                   })),
             })),
 
-          relations.albumGrid,
+          ([
+            !html.isBlank(relations.albumsBySeriesView),
+            !html.isBlank(relations.albumsByDateView)
+          ]).filter(Boolean).length > 1 &&
+
+            language.encapsulate(pageCapsule, 'albumViewSwitcher', capsule =>
+              html.tag('p', {class: 'gallery-view-switcher'},
+                {[html.onlyIfContent]: true},
+                {[html.joinChildren]: html.tag('br')},
+
+                [
+                  language.$(capsule),
+
+                  relations.albumViewSwitcher.slots({
+                    initialOptionIndex: 0,
+
+                    titles: [
+                      !html.isBlank(relations.albumsBySeriesView) &&
+                        language.$(capsule, 'bySeries'),
+
+                      !html.isBlank(relations.albumsByDateView) &&
+                        language.$(capsule, 'byDate'),
+                    ].filter(Boolean),
+
+                    targetIDs: [
+                      !html.isBlank(relations.albumsBySeriesView) &&
+                        'group-album-gallery-by-series',
+
+                      !html.isBlank(relations.albumsByDateView) &&
+                        'group-album-gallery-by-date',
+                    ].filter(Boolean),
+                  }),
+                ])),
+
+          /*
+          data.trackGridLabels.some(value => value !== null) &&
+            html.tag('p', {class: 'gallery-set-switcher'},
+              language.encapsulate(pageCapsule, 'setSwitcher', switcherCapsule =>
+                language.$(switcherCapsule, {
+                  sets:
+                    relations.setSwitcher.slots({
+                      initialOptionIndex: 0,
+
+                      titles:
+                        data.trackGridLabels.map(label =>
+                          label ??
+                          language.$(switcherCapsule, 'unlabeledSet')),
+
+                      targetIDs:
+                        data.trackGridIDs,
+                    }),
+                }))),
+          */
+
+          relations.albumsBySeriesView,
+
+          relations.albumsByDateView.slots({
+            attributes: [
+              !html.isBlank(relations.albumsBySeriesView) &&
+                {style: 'display: none'},
+            ],
+          }),
         ],
 
-        leftSidebar:
-          (relations.sidebar
-            ? relations.sidebar
-                .slot('currentExtra', 'gallery')
-                .content /* TODO: Kludge. */
-            : null),
-
         navLinkStyle: 'hierarchical',
         navLinks:
           relations.navLinks
             .slot('currentExtra', 'gallery')
             .content,
 
-        secondaryNav:
-          relations.secondaryNav ?? null,
+        secondaryNav: relations.secondaryNav,
       })),
 };
diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js
new file mode 100644
index 00000000..54f4b8cb
--- /dev/null
+++ b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js
@@ -0,0 +1,37 @@
+import {sortChronologically} from '#sort';
+
+export default {
+  contentDependencies: ['generateGroupGalleryPageAlbumGrid'],
+  extraDependencies: ['html', 'language'],
+
+  query: (group) => ({
+    albums:
+      sortChronologically(group.albums, {latestFirst: true}),
+  }),
+
+  relations: (relation, query, _group) => ({
+    albumGrid:
+      relation('generateGroupGalleryPageAlbumGrid', query.albums),
+  }),
+
+  slots: {
+    attributes: {
+      type: 'attributes',
+      mutable: false,
+    },
+  },
+
+  generate: (relations, slots, {html, language}) =>
+    language.encapsulate('groupGalleryPage.albumsByDate', capsule =>
+      html.tag('div', {id: 'group-album-gallery-by-date'},
+        slots.attributes,
+
+        {[html.onlyIfContent]: true},
+
+        html.tag('section', [
+          html.tag('h2',
+            language.$(capsule, 'title')),
+
+          relations.albumGrid,
+        ]))),
+};
diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js
new file mode 100644
index 00000000..0337275f
--- /dev/null
+++ b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js
@@ -0,0 +1,26 @@
+export default {
+  contentDependencies: ['generateGroupGalleryPageSeriesSection'],
+  extraDependencies: ['html'],
+
+  relations: (relation, group) => ({
+    seriesSections:
+      group.serieses
+        .map(series =>
+          relation('generateGroupGalleryPageSeriesSection', series)),
+  }),
+
+  slots: {
+    attributes: {
+      type: 'attributes',
+      mutable: false,
+    },
+  },
+
+  generate: (relations, slots, {html}) =>
+    html.tag('div', {id: 'group-album-gallery-by-series'},
+      slots.attributes,
+
+      {[html.onlyIfContent]: true},
+
+      relations.seriesSections),
+};
diff --git a/src/content/dependencies/generateGroupGalleryPageSeriesSection.js b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js
new file mode 100644
index 00000000..3fe3fb45
--- /dev/null
+++ b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js
@@ -0,0 +1,129 @@
+import {sortChronologically} from '#sort';
+
+export default {
+  contentDependencies: [
+    'generateExpandableGallerySection',
+    'generateGroupGalleryPageAlbumGrid',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  query(series) {
+    const query = {};
+
+    // Includes undated albums.
+    const albumsLatestFirst =
+      sortChronologically(series.albums, {latestFirst: true});
+
+    query.albumsAboveCut = albumsLatestFirst.slice(0, 4);
+    query.albumsBelowCut = albumsLatestFirst.slice(4);
+
+    query.allAlbumsDated =
+      series.albums.every(album => album.date);
+
+    query.latestAlbum =
+      albumsLatestFirst
+        .filter(album => album.date)
+        .at(0) ??
+      null;
+
+    query.earliestAlbum =
+      albumsLatestFirst
+        .filter(album => album.date)
+        .at(-1) ??
+      null;
+
+    return query;
+  },
+
+  relations: (relation, query, _series) => ({
+    gallerySection:
+      relation('generateExpandableGallerySection'),
+
+    gridAboveCut:
+      relation('generateGroupGalleryPageAlbumGrid', query.albumsAboveCut),
+
+    gridBelowCut:
+      relation('generateGroupGalleryPageAlbumGrid', query.albumsBelowCut),
+  }),
+
+  data: (query, series) => ({
+    name:
+      series.name,
+
+    albums:
+      series.albums.length,
+
+    tracks:
+      series.albums
+        .flatMap(album => album.tracks)
+        .length,
+
+    allAlbumsDated:
+      query.allAlbumsDated,
+
+    earliestAlbumDate:
+      (query.earliestAlbum
+        ? query.earliestAlbum.date
+        : null),
+
+    latestAlbumDate:
+      (query.latestAlbum
+        ? query.latestAlbum.date
+        : null),
+  }),
+
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('groupGalleryPage.albumSection', capsule =>
+      relations.gallerySection.slots({
+        title: data.name,
+
+        contentAboveCut: relations.gridAboveCut,
+        contentBelowCut: relations.gridBelowCut,
+
+        caption:
+          language.encapsulate(capsule, 'caption', workingCapsule => {
+            const workingOptions = {};
+
+            workingOptions.tracks =
+              html.tag('b',
+                language.countTracks(data.tracks, {unit: true}));
+
+            workingOptions.albums =
+              html.tag('b',
+                language.countAlbums(data.albums, {unit: true}));
+
+            if (data.allAlbumsDated) {
+              const earliestDate = data.earliestAlbumDate;
+              const latestDate = data.latestAlbumDate;
+
+              const earliestYear = earliestDate.getFullYear();
+              const latestYear = latestDate.getFullYear();
+
+              if (earliestYear === latestYear) {
+                if (data.albums === 1) {
+                  workingCapsule += '.withDate';
+                  workingOptions.date =
+                    language.formatDate(earliestDate);
+                } else {
+                  workingCapsule += '.withYear';
+                  workingOptions.year =
+                    language.formatYear(earliestDate);
+                }
+              } else {
+                workingCapsule += '.withYearRange';
+                workingOptions.yearRange =
+                  language.formatYearRange(earliestDate, latestDate);
+              }
+            }
+
+            return language.$(workingCapsule, workingOptions);
+          }),
+
+        expandCue:
+          language.$(capsule, 'expand'),
+
+        collapseCue:
+          language.$(capsule, 'collapse'),
+      })),
+};
diff --git a/src/data/things/language.js b/src/data/things/language.js
index a3f861bd..4e23cf7f 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -135,6 +135,7 @@ export class Language extends Thing {
     },
 
     intl_date: this.#intlHelper(Intl.DateTimeFormat, {full: true}),
+    intl_dateYear: this.#intlHelper(Intl.DateTimeFormat, {year: 'numeric'}),
     intl_number: this.#intlHelper(Intl.NumberFormat),
     intl_listConjunction: this.#intlHelper(Intl.ListFormat, {type: 'conjunction'}),
     intl_listDisjunction: this.#intlHelper(Intl.ListFormat, {type: 'disjunction'}),
@@ -488,22 +489,44 @@ export class Language extends Thing {
     // or both are undefined, that's just blank content.
     const hasStart = startDate !== null && startDate !== undefined;
     const hasEnd = endDate !== null && endDate !== undefined;
-    if (!hasStart || !hasEnd) {
-      if (startDate === endDate) {
-        return html.blank();
-      } else if (hasStart) {
-        throw new Error(`Expected both start and end of date range, got only start`);
-      } else if (hasEnd) {
-        throw new Error(`Expected both start and end of date range, got only end`);
-      } else {
-        throw new Error(`Got mismatched ${startDate}/${endDate} for start and end`);
-      }
+    if (!hasStart && !hasEnd) {
+      return html.blank();
+    } else if (hasStart && !hasEnd) {
+      throw new Error(`Expected both start and end of date range, got only start`);
+    } else if (!hasStart && hasEnd) {
+      throw new Error(`Expected both start and end of date range, got only end`);
     }
 
     this.assertIntlAvailable('intl_date');
     return this.intl_date.formatRange(startDate, endDate);
   }
 
+  formatYear(date) {
+    if (date === null || date === undefined) {
+      return html.blank();
+    }
+
+    this.assertIntlAvailable('intl_dateYear');
+    return this.intl_dateYear.format(date);
+  }
+
+  formatYearRange(startDate, endDate) {
+    // formatYearRange expects both values to be present, but if both are null
+    // or both are undefined, that's just blank content.
+    const hasStart = startDate !== null && startDate !== undefined;
+    const hasEnd = endDate !== null && endDate !== undefined;
+    if (!hasStart && !hasEnd) {
+      return html.blank();
+    } else if (hasStart && !hasEnd) {
+      throw new Error(`Expected both start and end of date range, got only start`);
+    } else if (!hasStart && hasEnd) {
+      throw new Error(`Expected both start and end of date range, got only end`);
+    }
+
+    this.assertIntlAvailable('intl_dateYear');
+    return this.intl_dateYear.formatRange(startDate, endDate);
+  }
+
   formatDateDuration({
     years: numYears = 0,
     months: numMonths = 0,
diff --git a/src/static/css/site.css b/src/static/css/site.css
index 655d095a..6fa4da38 100644
--- a/src/static/css/site.css
+++ b/src/static/css/site.css
@@ -1913,6 +1913,48 @@ ul.quick-info li:not(:last-child)::after {
   text-align: center;
 }
 
+.gallery-view-switcher {
+  text-align: center;
+  line-height: 1.4;
+}
+
+#content.top-index section {
+  margin-bottom: 1.5em;
+}
+
+.expandable-gallery-section .section-expando {
+  margin-top: 1em;
+  margin-bottom: 2em;
+
+  display: flex;
+  flex-direction: row;
+  justify-content: space-around;
+}
+
+.expandable-gallery-section .section-expando-content {
+  text-align: center;
+  line-height: 1.5;
+}
+
+.expandable-gallery-section .section-expando-toggle {
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
+.expandable-gallery-section.expanded .section-content-below-cut {
+  animation: expand-gallery-section 0.8s forwards;
+}
+
+@keyframes expand-gallery-section {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
 .quick-description:not(.has-external-links-only) {
   --clamped-padding-ratio: max(var(--responsive-padding-ratio), 0.06);
   margin-left: auto;
diff --git a/src/static/js/client/expandable-gallery-section.js b/src/static/js/client/expandable-gallery-section.js
new file mode 100644
index 00000000..dc83e8b7
--- /dev/null
+++ b/src/static/js/client/expandable-gallery-section.js
@@ -0,0 +1,77 @@
+/* eslint-env browser */
+
+// TODO: Combine this and quick-description.js
+
+import {cssProp} from '../client-util.js';
+
+import {stitchArrays} from '../../shared-util/sugar.js';
+
+export const info = {
+  id: 'expandableGallerySectionInfo',
+
+  sections: null,
+
+  sectionContentBelowCut: null,
+
+  sectionExpandoToggles: null,
+
+  sectionExpandCues: null,
+  sectionCollapseCues: null,
+};
+
+export function getPageReferences() {
+  info.sections =
+    Array.from(document.querySelectorAll('.expandable-gallery-section'))
+      .filter(section => section.querySelector('.section-expando-toggle'));
+
+  info.sectionContentBelowCut =
+    info.sections
+      .map(section => section.querySelector('.section-content-below-cut'));
+
+  info.sectionExpandoToggles =
+    info.sections
+      .map(section => section.querySelector('.section-expando-toggle'));
+
+  info.sectionExpandCues =
+    info.sections
+      .map(section => section.querySelector('.section-expand-cue'));
+
+  info.sectionCollapseCues =
+    info.sections
+      .map(section => section.querySelector('.section-collapse-cue'));
+}
+
+export function addPageListeners() {
+  for (const {
+    section,
+    contentBelowCut,
+    expandoToggle,
+    expandCue,
+    collapseCue,
+  } of stitchArrays({
+    section: info.sections,
+    contentBelowCut: info.sectionContentBelowCut,
+    expandoToggle: info.sectionExpandoToggles,
+    expandCue: info.sectionExpandCues,
+    collapseCue: info.sectionCollapseCues,
+  })) {
+    expandoToggle.addEventListener('click', domEvent => {
+      domEvent.preventDefault();
+
+      const collapsed =
+        cssProp(contentBelowCut, 'display') === 'none';
+
+      if (collapsed) {
+        section.classList.add('expanded');
+        cssProp(contentBelowCut, 'display', null);
+        cssProp(expandCue, 'display', 'none');
+        cssProp(collapseCue, 'display', null);
+      } else {
+        section.classList.remove('expanded');
+        cssProp(contentBelowCut, 'display', 'none');
+        cssProp(expandCue, 'display', null);
+        cssProp(collapseCue, 'display', 'none');
+      }
+    });
+  }
+}
diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js
index 81ea3415..aeb9264a 100644
--- a/src/static/js/client/index.js
+++ b/src/static/js/client/index.js
@@ -10,6 +10,7 @@ import * as artistExternalLinkTooltipModule from './artist-external-link-tooltip
 import * as cssCompatibilityAssistantModule from './css-compatibility-assistant.js';
 import * as datetimestampTooltipModule from './datetimestamp-tooltip.js';
 import * as draggedLinkModule from './dragged-link.js';
+import * as expandableGallerySectionModule from './expandable-gallery-section.js';
 import * as hashLinkModule from './hash-link.js';
 import * as hoverableTooltipModule from './hoverable-tooltip.js';
 import * as imageOverlayModule from './image-overlay.js';
@@ -32,6 +33,7 @@ export const modules = [
   cssCompatibilityAssistantModule,
   datetimestampTooltipModule,
   draggedLinkModule,
+  expandableGallerySectionModule,
   hashLinkModule,
   hoverableTooltipModule,
   imageOverlayModule,
diff --git a/src/static/js/client/quick-description.js b/src/static/js/client/quick-description.js
index cff82252..6a7a6023 100644
--- a/src/static/js/client/quick-description.js
+++ b/src/static/js/client/quick-description.js
@@ -1,5 +1,7 @@
 /* eslint-env browser */
 
+// TODO: Combine this and expandable-gallery-section.js
+
 import {stitchArrays} from '../../shared-util/sugar.js';
 
 export const info = {
diff --git a/src/strings-default.yaml b/src/strings-default.yaml
index 16687905..0f9bd54a 100644
--- a/src/strings-default.yaml
+++ b/src/strings-default.yaml
@@ -982,6 +982,8 @@ misc:
 
       otherCoverArtists: "With {ARTISTS}"
 
+    expandCollapseCue: "({CUE})"
+
   albumGalleryGrid:
     noCoverArt: "{NAME}"
 
@@ -1665,6 +1667,34 @@ groupGalleryPage:
   infoLine: >-
     {TRACKS} across {ALBUMS}, totaling {TIME}.
 
+  albumViewSwitcher:
+    _: "Showing albums:"
+
+    bySeries: "By series"
+    byDate: "By date"
+
+  albumsByDate:
+    title: "All albums"
+
+  albumSection:
+    caption: >-
+      {TRACKS} across {ALBUMS}.
+
+    caption.withDate: >-
+      {TRACKS} across {ALBUMS}, released {DATE}.
+
+    caption.withYear: >-
+      {TRACKS} across {ALBUMS}, released during {YEAR}.
+
+    caption.withYearRange: >-
+      {TRACKS} across {ALBUMS}, released {YEAR_RANGE}.
+
+    expand: >-
+      Show the rest!
+
+    collapse: >-
+      Collapse these
+
 #
 # listingIndex:
 #   The listing index page shows all available listings on the wiki,