1
0
Fork 0

Compare commits

..

2 Commits

Author SHA1 Message Date
Pabloader 9d50b7694b Worker cards design 2026-04-11 14:36:53 +00:00
Pabloader ea55a8fdf4 Format number function 2026-04-11 14:36:32 +00:00
6 changed files with 61 additions and 47 deletions

View File

@ -10,10 +10,13 @@
--text: #f8f8f2; --text: #f8f8f2;
--text-muted: #75715e; --text-muted: #75715e;
--text-dim: #cfcfc2; --text-dim: #cfcfc2;
--text-dark: #1a1a1a;
--yellow: #e6db74; --yellow: #e6db74;
--orange: #fd971f; --orange: #fd971f;
--blue: #66d9ef; --blue: #66d9ef;
--purple: #ae81ff; --purple: #ae81ff;
--green: #a3be8c;
--red: #e06c75;
--radius: 4px; --radius: 4px;
--transition: 0.15s ease; --transition: 0.15s ease;

View File

@ -1,5 +1,3 @@
const API_KEY = 'awoorwa32';
const saveThrottleDelay = 2000; const saveThrottleDelay = 2000;
const pendingSaves = new Map<string, ReturnType<typeof setTimeout>>(); const pendingSaves = new Map<string, ReturnType<typeof setTimeout>>();
@ -15,12 +13,10 @@ export const loadObject = async <T>(key: string, defaultObject: T): Promise<T> =
let remoteObject: Partial<T> = {}; let remoteObject: Partial<T> = {};
try { try {
const response = await fetch(`https://demo.pabloader.ru/storage/${key}?_=${Math.random()}`); // TODO loading from a remote source
if (response.ok) { const compressedData = new Blob([]);
const compressedData = await response.blob(); const decompressedData = await decompressBlob(compressedData);
const decompressedData = await decompressBlob(compressedData); remoteObject = JSON.parse(await decompressedData.text());
remoteObject = JSON.parse(await decompressedData.text());
}
} catch { } } catch { }
return { ...defaultObject, ...localObject, ...remoteObject }; return { ...defaultObject, ...localObject, ...remoteObject };
@ -43,22 +39,9 @@ const doSaveObject = async <T>(key: string, obj: T) => {
} catch { } } catch { }
try { try {
const url = new URL('https://demo.pabloader.ru/storage/index.php');
url.searchParams.set('filename', key);
const compressedData = await compressBlob(saveData); const compressedData = await compressBlob(saveData);
// TODO saving to remote storage
const response = await fetch(url, { void compressedData;
method: 'POST',
headers: {
'Content-Type': 'application/gzip',
'Authorization': `Bearer ${API_KEY}`,
},
body: compressedData,
});
if (!response.ok) {
throw new Error('Failed to save context');
}
} catch { } catch {
} }
} }
@ -68,14 +51,12 @@ export const compressBlob = async (blob: Blob | string): Promise<Blob> => {
blob = new Blob([blob]); blob = new Blob([blob]);
} }
const cs = new CompressionStream("gzip"); const cs = new CompressionStream("gzip");
// @ts-ignore
const compressedStream = blob.stream().pipeThrough(cs); const compressedStream = blob.stream().pipeThrough(cs);
return await new Response(compressedStream).blob(); return await new Response(compressedStream).blob();
} }
export const decompressBlob = async (blob: Blob): Promise<Blob> => { export const decompressBlob = async (blob: Blob): Promise<Blob> => {
const ds = new DecompressionStream("gzip"); const ds = new DecompressionStream("gzip");
// @ts-ignore
const decompressedStream = blob.stream().pipeThrough(ds); const decompressedStream = blob.stream().pipeThrough(ds);
return await new Response(decompressedStream).blob(); return await new Response(decompressedStream).blob();
} }

View File

@ -112,11 +112,17 @@ export const callUpdater = <T>(f: StateUpdater<T>, prev: T) =>
typeof f === 'function' ? (f as Function)(prev) : f; typeof f === 'function' ? (f as Function)(prev) : f;
export const formatNumber = (n: number): string => { export const formatNumber = (n: number): string => {
if (n === 0) return '0';
if (n < 0) return `-${formatNumber(-n)}`;
if (n >= 1_000_000_000_000) return `${(n / 1_000_000_000_000).toFixed(2)}T`; if (n >= 1_000_000_000_000) return `${(n / 1_000_000_000_000).toFixed(2)}T`;
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`; if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`; if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(2)}k`; if (n >= 1_000) return `${(n / 1_000).toFixed(2)}k`;
return String(n); if (Math.trunc(n) === n) return n.toString();
if (n >= 1) return n.toFixed(2);
const result = n.toPrecision(2);
if (parseFloat(result) >= 1) return parseFloat(result).toFixed(2);
return result;
}; };
export const formatTime = (seconds: number): string => { export const formatTime = (seconds: number): string => {

View File

@ -17,6 +17,15 @@
border-color: var(--accent-alt) !important; border-color: var(--accent-alt) !important;
} }
.maintenanceBorder {
border-color: var(--yellow) !important;
}
.ownBadge {
background: var(--accent-alt);
color: var(--text-dark);
}
.offline { .offline {
opacity: 0.5; opacity: 0.5;
} }
@ -65,24 +74,14 @@
text-transform: uppercase; text-transform: uppercase;
} }
.online {
background: var(--accent-alt);
color: #1a1a1a;
}
.offlineBadge {
background: var(--bg-active);
color: var(--text-muted);
}
.maintenance { .maintenance {
background: var(--yellow); background: var(--yellow);
color: #1a1a1a; color: var(--text-dark);
} }
.trusted { .trusted {
background: var(--blue); background: var(--accent);
color: #1a1a1a; color: var(--text-dark);
} }
.info { .info {

View File

@ -15,6 +15,7 @@ export const WorkerCard = ({ worker, isOwn }: Props) => {
return ( return (
<div class={clsx(styles.card, { <div class={clsx(styles.card, {
[styles.own]: isOwn, [styles.own]: isOwn,
[styles.maintenanceBorder]: worker.maintenance_mode,
[styles.offline]: !worker.online, [styles.offline]: !worker.online,
})}> })}>
<div class={styles.header}> <div class={styles.header}>
@ -29,14 +30,9 @@ export const WorkerCard = ({ worker, isOwn }: Props) => {
{worker.info ? <span class={styles.infoToggle}>{expanded.value ? ' ▲' : ' ▼'}</span> : null} {worker.info ? <span class={styles.infoToggle}>{expanded.value ? ' ▲' : ' ▼'}</span> : null}
</span> </span>
<div class={styles.badges}> <div class={styles.badges}>
<span class={clsx(styles.badge, { {isOwn && <span class={clsx(styles.badge, styles.ownBadge)}>own</span>}
[styles.online]: worker.online,
[styles.offlineBadge]: !worker.online,
})}>
{worker.online ? 'online' : 'offline'}
</span>
{worker.maintenance_mode && <span class={clsx(styles.badge, styles.maintenance)}>maintenance</span>} {worker.maintenance_mode && <span class={clsx(styles.badge, styles.maintenance)}>maintenance</span>}
{worker.trusted && <span class={clsx(styles.badge, styles.trusted)}>trusted</span>} {!worker.trusted && <span class={clsx(styles.badge, styles.trusted)}>not trusted</span>}
</div> </div>
</div> </div>
@ -77,6 +73,10 @@ export const WorkerCard = ({ worker, isOwn }: Props) => {
<dt>Kudos earned</dt> <dt>Kudos earned</dt>
<dd>{formatNumber(worker.kudos_rewards)}</dd> <dd>{formatNumber(worker.kudos_rewards)}</dd>
</div> </div>
<div class={styles.detail}>
<dt>Kudos/hour</dt>
<dd>{worker.uptime > 0 ? formatNumber(Math.round((worker.kudos_details.generated / worker.uptime) * 3600)) : '0'}</dd>
</div>
</dl> </dl>
</div> </div>
); );

View File

@ -310,12 +310,27 @@ describe('utils', () => {
}); });
describe('formatNumber', () => { describe('formatNumber', () => {
it('should return small numbers as-is', () => { it('should return small integer as-is', () => {
expect(formatNumber(0)).toBe('0'); expect(formatNumber(0)).toBe('0');
expect(formatNumber(42)).toBe('42'); expect(formatNumber(42)).toBe('42');
expect(formatNumber(999)).toBe('999'); expect(formatNumber(999)).toBe('999');
}); });
it('should return small decimals rounded to at most 2 significant digits', () => {
expect(formatNumber(0.1)).toBe('0.10');
expect(formatNumber(0.123)).toBe('0.12');
expect(formatNumber(0.5)).toBe('0.50');
expect(formatNumber(0.999)).toBe('1.00');
expect(formatNumber(0.001)).toBe('0.0010');
expect(formatNumber(0.0001234)).toBe('0.00012');
});
it('should return decimals between 100 and 1000 with 2 decimal places', () => {
expect(formatNumber(100.5)).toBe('100.50');
expect(formatNumber(500.75)).toBe('500.75');
expect(formatNumber(999.9)).toBe('999.90');
});
it('should format thousands with k suffix', () => { it('should format thousands with k suffix', () => {
expect(formatNumber(1_000)).toBe('1.00k'); expect(formatNumber(1_000)).toBe('1.00k');
expect(formatNumber(5_500)).toBe('5.50k'); expect(formatNumber(5_500)).toBe('5.50k');
@ -338,6 +353,16 @@ describe('utils', () => {
expect(formatNumber(1_000_000_000_000)).toBe('1.00T'); expect(formatNumber(1_000_000_000_000)).toBe('1.00T');
expect(formatNumber(5_250_000_000_000)).toBe('5.25T'); expect(formatNumber(5_250_000_000_000)).toBe('5.25T');
}); });
it('should format negative numbers with a leading minus sign', () => {
expect(formatNumber(-42)).toBe('-42');
expect(formatNumber(-0.123)).toBe('-0.12');
expect(formatNumber(-0.999)).toBe('-1.00');
expect(formatNumber(-1_000)).toBe('-1.00k');
expect(formatNumber(-1_000_000)).toBe('-1.00M');
expect(formatNumber(-1_000_000_000)).toBe('-1.00B');
expect(formatNumber(-1_000_000_000_000)).toBe('-1.00T');
});
}); });
describe('formatTime', () => { describe('formatTime', () => {