feat: rebuild the book page with the jacket layout

This commit is contained in:
2026-08-11 20:08:53 -04:00
parent f2b8f337f7
commit 4c3fd66a56
2 changed files with 479 additions and 388 deletions
+46
View File
@@ -58,6 +58,52 @@ export function formatFileSize(bytes: number, decimals = 1, binary = true) {
return `${formattedSize} ${units[exponent]}`;
}
/**
* Presentation for a book identifier.
*
* The backend stores identifiers as free-form name/value pairs, so this only
* decides how to label and link the ones we recognise. Anything unknown falls
* back to the stored name and renders as plain text.
*/
const IDENTIFIER_TYPES: Record<string, { label: string; url?: (value: string) => string }> = {
'isbn-13': { label: 'ISBN-13', url: (v) => `https://openlibrary.org/isbn/${isbnDigits(v)}` },
'isbn-10': { label: 'ISBN-10', url: (v) => `https://openlibrary.org/isbn/${isbnDigits(v)}` },
isbn: { label: 'ISBN', url: (v) => `https://openlibrary.org/isbn/${isbnDigits(v)}` },
doi: { label: 'DOI', url: (v) => `https://doi.org/${encodeURI(v.trim())}` },
asin: { label: 'ASIN', url: (v) => `https://www.amazon.com/dp/${encodeURIComponent(v.trim())}` }
};
/** Order the ones worth reading first; everything else keeps its own order. */
const IDENTIFIER_ORDER = ['isbn-13', 'isbn-10', 'isbn', 'doi', 'asin'];
function isbnDigits(value: string) {
return value.replace(/[^0-9Xx]/g, '');
}
function identifierKey(name: string) {
return name.trim().toLowerCase().replace(/[\s_]+/g, '-');
}
export function describeIdentifier(name: string, value: string) {
const known = IDENTIFIER_TYPES[identifierKey(name)];
return {
label: known?.label ?? name.toUpperCase(),
href: known?.url?.(value)
};
}
export function sortIdentifiers(identifiers: Record<string, string>) {
return Object.entries(identifiers).sort(([a], [b]) => {
const ia = IDENTIFIER_ORDER.indexOf(identifierKey(a));
const ib = IDENTIFIER_ORDER.indexOf(identifierKey(b));
if (ia === -1 && ib === -1) return 0;
if (ia === -1) return 1;
if (ib === -1) return -1;
return ia - ib;
});
}
export function getFileType(filename: string) {
const parts = filename.split('.');
if (parts.length < 2) return 'UNKNOWN';