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);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
  readonly bytes = localize.bytes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,8 +14,10 @@ import { CollectionThumbnail } from "./collection-thumbnail";
 | 
			
		||||
import { SelectCollectionAccess } from "./select-collection-access";
 | 
			
		||||
 | 
			
		||||
import { BtrixElement } from "@/classes/BtrixElement";
 | 
			
		||||
import { textSeparator } from "@/layouts/separator";
 | 
			
		||||
import { RouteNamespace } from "@/routes";
 | 
			
		||||
import type { PublicCollection } from "@/types/collection";
 | 
			
		||||
import { pluralOf } from "@/utils/pluralize";
 | 
			
		||||
import { tw } from "@/utils/tailwind";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -45,7 +47,7 @@ export class CollectionsGrid extends BtrixElement {
 | 
			
		||||
  pagination!: Node[];
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
      const thumb = html`
 | 
			
		||||
@ -114,14 +116,22 @@ export class CollectionsGrid extends BtrixElement {
 | 
			
		||||
                      ></sl-icon>`
 | 
			
		||||
                    : nothing}
 | 
			
		||||
                  <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}
 | 
			
		||||
                  </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 &&
 | 
			
		||||
                  html`
 | 
			
		||||
                    <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}
 | 
			
		||||
                    </p>
 | 
			
		||||
@ -180,7 +190,7 @@ export class CollectionsGrid extends BtrixElement {
 | 
			
		||||
    </div>
 | 
			
		||||
  `;
 | 
			
		||||
 | 
			
		||||
  private renderDateBadge(collection: PublicCollection) {
 | 
			
		||||
  renderDateBadge(collection: PublicCollection) {
 | 
			
		||||
    if (!collection.dateEarliest || !collection.dateLatest) return;
 | 
			
		||||
 | 
			
		||||
    const earliestYear = this.localize.date(collection.dateEarliest, {
 | 
			
		||||
@ -190,10 +200,32 @@ export class CollectionsGrid extends BtrixElement {
 | 
			
		||||
      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`
 | 
			
		||||
      <btrix-badge variant="primary" class="absolute right-3 top-3">
 | 
			
		||||
        ${earliestYear}
 | 
			
		||||
        ${latestYear !== earliestYear ? html` – ${latestYear} ` : nothing}
 | 
			
		||||
        ${date}
 | 
			
		||||
      </btrix-badge>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -52,6 +52,10 @@ export function metadataColumn(collection?: Collection | PublicCollection) {
 | 
			
		||||
        render: (col) =>
 | 
			
		||||
          `${localize.number(col.pageCount)} ${pluralOf("pages", col.pageCount)}`,
 | 
			
		||||
      })}
 | 
			
		||||
      ${metadataItem({
 | 
			
		||||
        label: metadata.totalSize,
 | 
			
		||||
        render: (col) => `${localize.bytes(col.totalSize)}`,
 | 
			
		||||
      })}
 | 
			
		||||
    </btrix-desc-list>
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,8 @@ import clsx from "clsx";
 | 
			
		||||
import { html, nothing, type TemplateResult } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import { textSeparator } from "./separator";
 | 
			
		||||
 | 
			
		||||
import { NavigateController } from "@/controllers/navigate";
 | 
			
		||||
import { tw } from "@/utils/tailwind";
 | 
			
		||||
 | 
			
		||||
@ -26,15 +28,7 @@ function navigateBreadcrumb(e: MouseEvent, href: string) {
 | 
			
		||||
  el.dispatchEvent(evt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function breadcrumbSeparator() {
 | 
			
		||||
  return html`<span
 | 
			
		||||
    class="font-mono font-thin text-neutral-400"
 | 
			
		||||
    role="separator"
 | 
			
		||||
    >/</span
 | 
			
		||||
  > `;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const separator = breadcrumbSeparator();
 | 
			
		||||
const breadcrumbSeparator = textSeparator();
 | 
			
		||||
const skeleton = html`<sl-skeleton class="w-48"></sl-skeleton>`;
 | 
			
		||||
 | 
			
		||||
function breadcrumbLink({ href, content }: Breadcrumb, classNames?: string) {
 | 
			
		||||
@ -65,11 +59,11 @@ function pageBreadcrumbs(breadcrumbs: Breadcrumb[]) {
 | 
			
		||||
      ${breadcrumbs.length
 | 
			
		||||
        ? breadcrumbs.map(
 | 
			
		||||
            (breadcrumb, i) => html`
 | 
			
		||||
              ${i !== 0 ? separator : nothing}
 | 
			
		||||
              ${i !== 0 ? breadcrumbSeparator : nothing}
 | 
			
		||||
              ${breadcrumbLink(breadcrumb, tw`max-w-[30ch]`)}
 | 
			
		||||
            `,
 | 
			
		||||
          )
 | 
			
		||||
        : html`${skeleton} ${separator} ${skeleton}`}
 | 
			
		||||
        : html`${skeleton} ${breadcrumbSeparator} ${skeleton}`}
 | 
			
		||||
    </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"),
 | 
			
		||||
  uniquePageCount: msg("Unique Pages in Collection"),
 | 
			
		||||
  pageCount: msg("Total Pages Crawled"),
 | 
			
		||||
  totalSize: msg("Collection Size"),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -264,6 +264,38 @@ export class Localize {
 | 
			
		||||
    const pluralRule = formatter.select(value);
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user