init new frontend
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
/** Display helpers shared across list views. */
|
||||
|
||||
/**
|
||||
* Map a legacy `icon_class` (e.g. "fas fa-folder", "fa-file-pdf") to an icon
|
||||
* registry name (the FA token without the `fa-` prefix).
|
||||
*/
|
||||
export function iconNameFromClass(iconClass: string | undefined | null): string {
|
||||
if (!iconClass) return 'file';
|
||||
const token = iconClass
|
||||
.split(/\s+/)
|
||||
.find((c) => c.startsWith('fa-') && c !== 'fa-fw' && c !== 'fa-lg');
|
||||
return token ? token.slice(3) : 'file';
|
||||
}
|
||||
|
||||
/** Format a timestamp (epoch seconds/ms or ISO-8601 string) as a local date. */
|
||||
export function formatDate(value: number | string | null | undefined): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
let d: Date;
|
||||
if (typeof value === 'number') {
|
||||
// Heuristic: seconds vs milliseconds.
|
||||
d = new Date(value < 1e12 ? value * 1000 : value);
|
||||
} else {
|
||||
d = new Date(value);
|
||||
}
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatBytes } from './format';
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('formats zero', () => {
|
||||
expect(formatBytes(0)).toBe('0 B');
|
||||
});
|
||||
|
||||
it('formats bytes without decimals', () => {
|
||||
expect(formatBytes(512)).toBe('512 B');
|
||||
});
|
||||
|
||||
it('formats kilobytes and megabytes', () => {
|
||||
expect(formatBytes(1024)).toBe('1.0 KB');
|
||||
expect(formatBytes(1536)).toBe('1.5 KB');
|
||||
expect(formatBytes(5 * 1024 * 1024)).toBe('5.0 MB');
|
||||
});
|
||||
|
||||
it('handles invalid input', () => {
|
||||
expect(formatBytes(-1)).toBe('—');
|
||||
expect(formatBytes(NaN)).toBe('—');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Format a byte count as a human-readable size string.
|
||||
* Seed utility to validate the unit-test harness; the full formatter set is
|
||||
* ported from static/js/core/formatters.js in Phase 1.
|
||||
*/
|
||||
export function formatBytes(bytes: number, decimals = 1): string {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return '—';
|
||||
if (bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||
const value = bytes / Math.pow(1024, i);
|
||||
return `${value.toFixed(i === 0 ? 0 : decimals)} ${units[i]}`;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { legacyHashToPath } from './legacyHash';
|
||||
|
||||
describe('legacyHashToPath', () => {
|
||||
it('maps root and files', () => {
|
||||
expect(legacyHashToPath('#/')).toBe('/files');
|
||||
expect(legacyHashToPath('#/files')).toBe('/files');
|
||||
});
|
||||
|
||||
it('maps folder deep links to the new path', () => {
|
||||
expect(legacyHashToPath('#/files/folder/abc')).toBe('/files/abc');
|
||||
expect(legacyHashToPath('#/files/folder/abc/def')).toBe('/files/abc/def');
|
||||
});
|
||||
|
||||
it('maps the named sections', () => {
|
||||
expect(legacyHashToPath('#/shared')).toBe('/shared');
|
||||
expect(legacyHashToPath('#/sharedwithme')).toBe('/shared-with-me');
|
||||
expect(legacyHashToPath('#/recent')).toBe('/recent');
|
||||
expect(legacyHashToPath('#/favorites')).toBe('/favorites');
|
||||
expect(legacyHashToPath('#/trash')).toBe('/trash');
|
||||
expect(legacyHashToPath('#/photos')).toBe('/photos');
|
||||
expect(legacyHashToPath('#/music')).toBe('/music');
|
||||
});
|
||||
|
||||
it('ignores query strings in the hash', () => {
|
||||
expect(legacyHashToPath('#/recent?foo=bar')).toBe('/recent');
|
||||
});
|
||||
|
||||
it('returns null for non-legacy or unknown hashes', () => {
|
||||
expect(legacyHashToPath('')).toBeNull();
|
||||
expect(legacyHashToPath('#section')).toBeNull();
|
||||
expect(legacyHashToPath('#/unknown')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Translate a legacy hash route (`#/...` from the vanilla app) into the new
|
||||
* SvelteKit path, so old bookmarks and external links keep working.
|
||||
*
|
||||
* Returns the new pathname, or null when the hash isn't a legacy route.
|
||||
*/
|
||||
export function legacyHashToPath(hash: string): string | null {
|
||||
if (!hash.startsWith('#/')) return null;
|
||||
const raw = hash.slice(1); // drop the '#'
|
||||
const [pathPart] = raw.split('?');
|
||||
const segs = pathPart.split('/').filter(Boolean); // e.g. ['files','folder','<id>']
|
||||
|
||||
if (segs.length === 0 || segs[0] === 'files') {
|
||||
// #/ , #/files , #/files/folder/<id>/<id>...
|
||||
if (segs[1] === 'folder') return `/files/${segs.slice(2).join('/')}`;
|
||||
return '/files';
|
||||
}
|
||||
|
||||
switch (segs[0]) {
|
||||
case 'shared':
|
||||
return '/shared';
|
||||
case 'sharedwithme':
|
||||
return '/shared-with-me';
|
||||
case 'recent':
|
||||
return '/recent';
|
||||
case 'favorites':
|
||||
return '/favorites';
|
||||
case 'trash':
|
||||
return '/trash';
|
||||
case 'photos':
|
||||
return '/photos';
|
||||
case 'music':
|
||||
return '/music';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user