init new frontend

This commit is contained in:
Bradley Nelson
2026-06-17 17:06:30 -06:00
parent b8a0018785
commit daa3010458
114 changed files with 32716 additions and 119 deletions
+27
View File
@@ -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' });
}
+23
View File
@@ -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('—');
});
});
+13
View File
@@ -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]}`;
}
+34
View File
@@ -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();
});
});
+37
View File
@@ -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;
}
}