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