feat: Show additional collection details (#2455)
Resolves https://github.com/webrecorder/browsertrix/issues/2452 ## Changes - Displays page count and collection size in listing grid - Displays month if collection period is in the same year - Displays collection size in About > Details section - Minor refactor: move byte formatting into `localize.ts` utility file, move slash (`/`) separator into own utility file
This commit is contained in:
		
							parent
							
								
									e13c3bfb48
								
							
						
					
					
						commit
						65a40c4816
					
				@ -33,35 +33,5 @@ export class LocalizeController extends SlLocalizeController {
 | 
				
			|||||||
    return localize.duration(duration);
 | 
					    return localize.duration(duration);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  readonly bytes = localize.bytes;
 | 
				
			||||||
   * From https://github.com/shoelace-style/shoelace/blob/v2.18.0/src/components/format-bytes/format-bytes.component.ts
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  readonly bytes = (value: number, options?: Intl.NumberFormatOptions) => {
 | 
					 | 
				
			||||||
    if (isNaN(value)) {
 | 
					 | 
				
			||||||
      return "";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const opts: Intl.NumberFormatOptions = {
 | 
					 | 
				
			||||||
      unit: "byte",
 | 
					 | 
				
			||||||
      unitDisplay: "short",
 | 
					 | 
				
			||||||
      ...options,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    const bitPrefixes = ["", "kilo", "mega", "giga", "tera"]; // petabit isn't a supported unit
 | 
					 | 
				
			||||||
    const bytePrefixes = ["", "kilo", "mega", "giga", "tera", "peta"];
 | 
					 | 
				
			||||||
    const prefix = opts.unit === "bit" ? bitPrefixes : bytePrefixes;
 | 
					 | 
				
			||||||
    const index = Math.max(
 | 
					 | 
				
			||||||
      0,
 | 
					 | 
				
			||||||
      Math.min(Math.floor(Math.log10(value) / 3), prefix.length - 1),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    const unit = prefix[index] + opts.unit;
 | 
					 | 
				
			||||||
    const valueToFormat = parseFloat(
 | 
					 | 
				
			||||||
      (value / Math.pow(1000, index)).toPrecision(3),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return localize.number(valueToFormat, {
 | 
					 | 
				
			||||||
      style: "unit",
 | 
					 | 
				
			||||||
      unit,
 | 
					 | 
				
			||||||
      unitDisplay: opts.unitDisplay,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -14,8 +14,10 @@ import { CollectionThumbnail } from "./collection-thumbnail";
 | 
				
			|||||||
import { SelectCollectionAccess } from "./select-collection-access";
 | 
					import { SelectCollectionAccess } from "./select-collection-access";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { BtrixElement } from "@/classes/BtrixElement";
 | 
					import { BtrixElement } from "@/classes/BtrixElement";
 | 
				
			||||||
 | 
					import { textSeparator } from "@/layouts/separator";
 | 
				
			||||||
import { RouteNamespace } from "@/routes";
 | 
					import { RouteNamespace } from "@/routes";
 | 
				
			||||||
import type { PublicCollection } from "@/types/collection";
 | 
					import type { PublicCollection } from "@/types/collection";
 | 
				
			||||||
 | 
					import { pluralOf } from "@/utils/pluralize";
 | 
				
			||||||
import { tw } from "@/utils/tailwind";
 | 
					import { tw } from "@/utils/tailwind";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -45,7 +47,7 @@ export class CollectionsGrid extends BtrixElement {
 | 
				
			|||||||
  pagination!: Node[];
 | 
					  pagination!: Node[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
					  render() {
 | 
				
			||||||
    const gridClassNames = tw`grid flex-1 grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`;
 | 
					    const gridClassNames = tw`grid flex-1 grid-cols-1 gap-10 md:grid-cols-2 lg:grid-cols-3`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!this.collections || !this.slug) {
 | 
					    if (!this.collections || !this.slug) {
 | 
				
			||||||
      const thumb = html`
 | 
					      const thumb = html`
 | 
				
			||||||
@ -114,14 +116,22 @@ export class CollectionsGrid extends BtrixElement {
 | 
				
			|||||||
                      ></sl-icon>`
 | 
					                      ></sl-icon>`
 | 
				
			||||||
                    : nothing}
 | 
					                    : nothing}
 | 
				
			||||||
                  <strong
 | 
					                  <strong
 | 
				
			||||||
                    class="text-base font-medium leading-tight text-stone-700 transition-colors group-hover:text-cyan-600"
 | 
					                    class="text-base font-medium leading-tight text-stone-800 transition-colors group-hover:text-cyan-600"
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    ${collection.name}
 | 
					                    ${collection.name}
 | 
				
			||||||
                  </strong>
 | 
					                  </strong>
 | 
				
			||||||
 | 
					                  <div class="mt-1.5 flex gap-2 leading-tight text-stone-400">
 | 
				
			||||||
 | 
					                    <div>
 | 
				
			||||||
 | 
					                      ${this.localize.number(collection.pageCount)}
 | 
				
			||||||
 | 
					                      ${pluralOf("pages", collection.pageCount)}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    ${textSeparator()}
 | 
				
			||||||
 | 
					                    <div>${this.localize.bytes(collection.totalSize)}</div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
                  ${collection.caption &&
 | 
					                  ${collection.caption &&
 | 
				
			||||||
                  html`
 | 
					                  html`
 | 
				
			||||||
                    <p
 | 
					                    <p
 | 
				
			||||||
                      class="mt-1.5 text-pretty leading-relaxed text-stone-500 transition-colors group-hover:text-cyan-600"
 | 
					                      class="mt-1.5 text-pretty leading-relaxed text-stone-500"
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                      ${collection.caption}
 | 
					                      ${collection.caption}
 | 
				
			||||||
                    </p>
 | 
					                    </p>
 | 
				
			||||||
@ -180,7 +190,7 @@ export class CollectionsGrid extends BtrixElement {
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  `;
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private renderDateBadge(collection: PublicCollection) {
 | 
					  renderDateBadge(collection: PublicCollection) {
 | 
				
			||||||
    if (!collection.dateEarliest || !collection.dateLatest) return;
 | 
					    if (!collection.dateEarliest || !collection.dateLatest) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const earliestYear = this.localize.date(collection.dateEarliest, {
 | 
					    const earliestYear = this.localize.date(collection.dateEarliest, {
 | 
				
			||||||
@ -190,10 +200,32 @@ export class CollectionsGrid extends BtrixElement {
 | 
				
			|||||||
      year: "numeric",
 | 
					      year: "numeric",
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let date = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (earliestYear === latestYear) {
 | 
				
			||||||
 | 
					      const earliestMonth = new Date(collection.dateEarliest).getMonth();
 | 
				
			||||||
 | 
					      const latestMonth = new Date(collection.dateLatest).getMonth();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (earliestMonth === latestMonth) {
 | 
				
			||||||
 | 
					        date = this.localize.date(collection.dateEarliest, {
 | 
				
			||||||
 | 
					          month: "long",
 | 
				
			||||||
 | 
					          year: "numeric",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        date = `${this.localize.date(collection.dateEarliest, {
 | 
				
			||||||
 | 
					          month: "short",
 | 
				
			||||||
 | 
					        })} – ${this.localize.date(collection.dateLatest, {
 | 
				
			||||||
 | 
					          month: "short",
 | 
				
			||||||
 | 
					          year: "numeric",
 | 
				
			||||||
 | 
					        })}`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      date = `${earliestYear} – ${latestYear} `;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <btrix-badge variant="primary" class="absolute right-3 top-3">
 | 
					      <btrix-badge variant="primary" class="absolute right-3 top-3">
 | 
				
			||||||
        ${earliestYear}
 | 
					        ${date}
 | 
				
			||||||
        ${latestYear !== earliestYear ? html` – ${latestYear} ` : nothing}
 | 
					 | 
				
			||||||
      </btrix-badge>
 | 
					      </btrix-badge>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -52,6 +52,10 @@ export function metadataColumn(collection?: Collection | PublicCollection) {
 | 
				
			|||||||
        render: (col) =>
 | 
					        render: (col) =>
 | 
				
			||||||
          `${localize.number(col.pageCount)} ${pluralOf("pages", col.pageCount)}`,
 | 
					          `${localize.number(col.pageCount)} ${pluralOf("pages", col.pageCount)}`,
 | 
				
			||||||
      })}
 | 
					      })}
 | 
				
			||||||
 | 
					      ${metadataItem({
 | 
				
			||||||
 | 
					        label: metadata.totalSize,
 | 
				
			||||||
 | 
					        render: (col) => `${localize.bytes(col.totalSize)}`,
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
    </btrix-desc-list>
 | 
					    </btrix-desc-list>
 | 
				
			||||||
  `;
 | 
					  `;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,8 @@ import clsx from "clsx";
 | 
				
			|||||||
import { html, nothing, type TemplateResult } from "lit";
 | 
					import { html, nothing, type TemplateResult } from "lit";
 | 
				
			||||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
					import { ifDefined } from "lit/directives/if-defined.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { textSeparator } from "./separator";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { NavigateController } from "@/controllers/navigate";
 | 
					import { NavigateController } from "@/controllers/navigate";
 | 
				
			||||||
import { tw } from "@/utils/tailwind";
 | 
					import { tw } from "@/utils/tailwind";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -26,15 +28,7 @@ function navigateBreadcrumb(e: MouseEvent, href: string) {
 | 
				
			|||||||
  el.dispatchEvent(evt);
 | 
					  el.dispatchEvent(evt);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function breadcrumbSeparator() {
 | 
					const breadcrumbSeparator = textSeparator();
 | 
				
			||||||
  return html`<span
 | 
					 | 
				
			||||||
    class="font-mono font-thin text-neutral-400"
 | 
					 | 
				
			||||||
    role="separator"
 | 
					 | 
				
			||||||
    >/</span
 | 
					 | 
				
			||||||
  > `;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const separator = breadcrumbSeparator();
 | 
					 | 
				
			||||||
const skeleton = html`<sl-skeleton class="w-48"></sl-skeleton>`;
 | 
					const skeleton = html`<sl-skeleton class="w-48"></sl-skeleton>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function breadcrumbLink({ href, content }: Breadcrumb, classNames?: string) {
 | 
					function breadcrumbLink({ href, content }: Breadcrumb, classNames?: string) {
 | 
				
			||||||
@ -65,11 +59,11 @@ function pageBreadcrumbs(breadcrumbs: Breadcrumb[]) {
 | 
				
			|||||||
      ${breadcrumbs.length
 | 
					      ${breadcrumbs.length
 | 
				
			||||||
        ? breadcrumbs.map(
 | 
					        ? breadcrumbs.map(
 | 
				
			||||||
            (breadcrumb, i) => html`
 | 
					            (breadcrumb, i) => html`
 | 
				
			||||||
              ${i !== 0 ? separator : nothing}
 | 
					              ${i !== 0 ? breadcrumbSeparator : nothing}
 | 
				
			||||||
              ${breadcrumbLink(breadcrumb, tw`max-w-[30ch]`)}
 | 
					              ${breadcrumbLink(breadcrumb, tw`max-w-[30ch]`)}
 | 
				
			||||||
            `,
 | 
					            `,
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        : html`${skeleton} ${separator} ${skeleton}`}
 | 
					        : html`${skeleton} ${breadcrumbSeparator} ${skeleton}`}
 | 
				
			||||||
    </nav>
 | 
					    </nav>
 | 
				
			||||||
  `;
 | 
					  `;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								frontend/src/layouts/separator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/layouts/separator.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { html } from "lit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * For separatoring text in the same line, e.g. for breadcrumbs or item details
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function textSeparator() {
 | 
				
			||||||
 | 
					  return html`<span
 | 
				
			||||||
 | 
					    class="font-mono font-thin text-neutral-400"
 | 
				
			||||||
 | 
					    role="separator"
 | 
				
			||||||
 | 
					    >/</span
 | 
				
			||||||
 | 
					  > `;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -4,4 +4,5 @@ export const metadata = {
 | 
				
			|||||||
  dateLatest: msg("Collection Period"),
 | 
					  dateLatest: msg("Collection Period"),
 | 
				
			||||||
  uniquePageCount: msg("Unique Pages in Collection"),
 | 
					  uniquePageCount: msg("Unique Pages in Collection"),
 | 
				
			||||||
  pageCount: msg("Total Pages Crawled"),
 | 
					  pageCount: msg("Total Pages Crawled"),
 | 
				
			||||||
 | 
					  totalSize: msg("Collection Size"),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -264,6 +264,38 @@ export class Localize {
 | 
				
			|||||||
    const pluralRule = formatter.select(value);
 | 
					    const pluralRule = formatter.select(value);
 | 
				
			||||||
    return phrases[pluralRule];
 | 
					    return phrases[pluralRule];
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * From https://github.com/shoelace-style/shoelace/blob/v2.18.0/src/components/format-bytes/format-bytes.component.ts
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  readonly bytes = (value: number, options?: Intl.NumberFormatOptions) => {
 | 
				
			||||||
 | 
					    if (isNaN(value)) {
 | 
				
			||||||
 | 
					      return "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const opts: Intl.NumberFormatOptions = {
 | 
				
			||||||
 | 
					      unit: "byte",
 | 
				
			||||||
 | 
					      unitDisplay: "short",
 | 
				
			||||||
 | 
					      ...options,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const bitPrefixes = ["", "kilo", "mega", "giga", "tera"]; // petabit isn't a supported unit
 | 
				
			||||||
 | 
					    const bytePrefixes = ["", "kilo", "mega", "giga", "tera", "peta"];
 | 
				
			||||||
 | 
					    const prefix = opts.unit === "bit" ? bitPrefixes : bytePrefixes;
 | 
				
			||||||
 | 
					    const index = Math.max(
 | 
				
			||||||
 | 
					      0,
 | 
				
			||||||
 | 
					      Math.min(Math.floor(Math.log10(value) / 3), prefix.length - 1),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const unit = prefix[index] + opts.unit;
 | 
				
			||||||
 | 
					    const valueToFormat = parseFloat(
 | 
				
			||||||
 | 
					      (value / Math.pow(1000, index)).toPrecision(3),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return localize.number(valueToFormat, {
 | 
				
			||||||
 | 
					      style: "unit",
 | 
				
			||||||
 | 
					      unit,
 | 
				
			||||||
 | 
					      unitDisplay: opts.unitDisplay,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const localize = new Localize(sourceLocale);
 | 
					const localize = new Localize(sourceLocale);
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user