From 65a40c48161be18b0883520afca074f2fbadf9c0 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Mon, 3 Mar 2025 13:15:27 -0800 Subject: [PATCH] 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 --- frontend/src/controllers/localize.ts | 32 +------------- .../features/collections/collections-grid.ts | 44 ++++++++++++++++--- .../src/layouts/collections/metadataColumn.ts | 4 ++ frontend/src/layouts/pageHeader.ts | 16 +++---- frontend/src/layouts/separator.ts | 12 +++++ frontend/src/strings/collections/metadata.ts | 1 + frontend/src/utils/localize.ts | 32 ++++++++++++++ 7 files changed, 93 insertions(+), 48 deletions(-) create mode 100644 frontend/src/layouts/separator.ts diff --git a/frontend/src/controllers/localize.ts b/frontend/src/controllers/localize.ts index cf61b2ac..fe3b2c86 100644 --- a/frontend/src/controllers/localize.ts +++ b/frontend/src/controllers/localize.ts @@ -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; } diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts index 3d87906b..7675e32f 100644 --- a/frontend/src/features/collections/collections-grid.ts +++ b/frontend/src/features/collections/collections-grid.ts @@ -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 { >` : nothing} ${collection.name} +
+
+ ${this.localize.number(collection.pageCount)} + ${pluralOf("pages", collection.pageCount)} +
+ ${textSeparator()} +
${this.localize.bytes(collection.totalSize)}
+
${collection.caption && html`

${collection.caption}

@@ -180,7 +190,7 @@ export class CollectionsGrid extends BtrixElement { `; - 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` - ${earliestYear} - ${latestYear !== earliestYear ? html` – ${latestYear} ` : nothing} + ${date} `; } diff --git a/frontend/src/layouts/collections/metadataColumn.ts b/frontend/src/layouts/collections/metadataColumn.ts index e1ac449f..3207e924 100644 --- a/frontend/src/layouts/collections/metadataColumn.ts +++ b/frontend/src/layouts/collections/metadataColumn.ts @@ -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)}`, + })} `; } diff --git a/frontend/src/layouts/pageHeader.ts b/frontend/src/layouts/pageHeader.ts index ae47dd19..f0522b9d 100644 --- a/frontend/src/layouts/pageHeader.ts +++ b/frontend/src/layouts/pageHeader.ts @@ -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`/ `; -} - -const separator = breadcrumbSeparator(); +const breadcrumbSeparator = textSeparator(); const skeleton = html``; 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}`} `; } diff --git a/frontend/src/layouts/separator.ts b/frontend/src/layouts/separator.ts new file mode 100644 index 00000000..1ab2bd17 --- /dev/null +++ b/frontend/src/layouts/separator.ts @@ -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`/ `; +} diff --git a/frontend/src/strings/collections/metadata.ts b/frontend/src/strings/collections/metadata.ts index 1f7d906e..39805563 100644 --- a/frontend/src/strings/collections/metadata.ts +++ b/frontend/src/strings/collections/metadata.ts @@ -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"), }; diff --git a/frontend/src/utils/localize.ts b/frontend/src/utils/localize.ts index 8a45771d..f6f2a6e2 100644 --- a/frontend/src/utils/localize.ts +++ b/frontend/src/utils/localize.ts @@ -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);