Compare commits
2 Commits
c939ab13c5
...
9d50b7694b
| Author | SHA1 | Date |
|---|---|---|
|
|
9d50b7694b | |
|
|
ea55a8fdf4 |
|
|
@ -10,10 +10,13 @@
|
|||
--text: #f8f8f2;
|
||||
--text-muted: #75715e;
|
||||
--text-dim: #cfcfc2;
|
||||
--text-dark: #1a1a1a;
|
||||
--yellow: #e6db74;
|
||||
--orange: #fd971f;
|
||||
--blue: #66d9ef;
|
||||
--purple: #ae81ff;
|
||||
--green: #a3be8c;
|
||||
--red: #e06c75;
|
||||
|
||||
--radius: 4px;
|
||||
--transition: 0.15s ease;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
const API_KEY = 'awoorwa32';
|
||||
|
||||
const saveThrottleDelay = 2000;
|
||||
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> = {};
|
||||
try {
|
||||
const response = await fetch(`https://demo.pabloader.ru/storage/${key}?_=${Math.random()}`);
|
||||
if (response.ok) {
|
||||
const compressedData = await response.blob();
|
||||
// TODO loading from a remote source
|
||||
const compressedData = new Blob([]);
|
||||
const decompressedData = await decompressBlob(compressedData);
|
||||
remoteObject = JSON.parse(await decompressedData.text());
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return { ...defaultObject, ...localObject, ...remoteObject };
|
||||
|
|
@ -43,22 +39,9 @@ const doSaveObject = async <T>(key: string, obj: T) => {
|
|||
} catch { }
|
||||
|
||||
try {
|
||||
const url = new URL('https://demo.pabloader.ru/storage/index.php');
|
||||
url.searchParams.set('filename', key);
|
||||
|
||||
const compressedData = await compressBlob(saveData);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/gzip',
|
||||
'Authorization': `Bearer ${API_KEY}`,
|
||||
},
|
||||
body: compressedData,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save context');
|
||||
}
|
||||
// TODO saving to remote storage
|
||||
void compressedData;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
|
@ -68,14 +51,12 @@ export const compressBlob = async (blob: Blob | string): Promise<Blob> => {
|
|||
blob = new Blob([blob]);
|
||||
}
|
||||
const cs = new CompressionStream("gzip");
|
||||
// @ts-ignore
|
||||
const compressedStream = blob.stream().pipeThrough(cs);
|
||||
return await new Response(compressedStream).blob();
|
||||
}
|
||||
|
||||
export const decompressBlob = async (blob: Blob): Promise<Blob> => {
|
||||
const ds = new DecompressionStream("gzip");
|
||||
// @ts-ignore
|
||||
const decompressedStream = blob.stream().pipeThrough(ds);
|
||||
return await new Response(decompressedStream).blob();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,11 +112,17 @@ export const callUpdater = <T>(f: StateUpdater<T>, prev: T) =>
|
|||
typeof f === 'function' ? (f as Function)(prev) : f;
|
||||
|
||||
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) 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) 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 => {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@
|
|||
border-color: var(--accent-alt) !important;
|
||||
}
|
||||
|
||||
.maintenanceBorder {
|
||||
border-color: var(--yellow) !important;
|
||||
}
|
||||
|
||||
.ownBadge {
|
||||
background: var(--accent-alt);
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.offline {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
@ -65,24 +74,14 @@
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.online {
|
||||
background: var(--accent-alt);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.offlineBadge {
|
||||
background: var(--bg-active);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
background: var(--yellow);
|
||||
color: #1a1a1a;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.trusted {
|
||||
background: var(--blue);
|
||||
color: #1a1a1a;
|
||||
background: var(--accent);
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.info {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const WorkerCard = ({ worker, isOwn }: Props) => {
|
|||
return (
|
||||
<div class={clsx(styles.card, {
|
||||
[styles.own]: isOwn,
|
||||
[styles.maintenanceBorder]: worker.maintenance_mode,
|
||||
[styles.offline]: !worker.online,
|
||||
})}>
|
||||
<div class={styles.header}>
|
||||
|
|
@ -29,14 +30,9 @@ export const WorkerCard = ({ worker, isOwn }: Props) => {
|
|||
{worker.info ? <span class={styles.infoToggle}>{expanded.value ? ' ▲' : ' ▼'}</span> : null}
|
||||
</span>
|
||||
<div class={styles.badges}>
|
||||
<span class={clsx(styles.badge, {
|
||||
[styles.online]: worker.online,
|
||||
[styles.offlineBadge]: !worker.online,
|
||||
})}>
|
||||
{worker.online ? 'online' : 'offline'}
|
||||
</span>
|
||||
{isOwn && <span class={clsx(styles.badge, styles.ownBadge)}>own</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>
|
||||
|
||||
|
|
@ -77,6 +73,10 @@ export const WorkerCard = ({ worker, isOwn }: Props) => {
|
|||
<dt>Kudos earned</dt>
|
||||
<dd>{formatNumber(worker.kudos_rewards)}</dd>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -310,12 +310,27 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
describe('formatNumber', () => {
|
||||
it('should return small numbers as-is', () => {
|
||||
it('should return small integer as-is', () => {
|
||||
expect(formatNumber(0)).toBe('0');
|
||||
expect(formatNumber(42)).toBe('42');
|
||||
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', () => {
|
||||
expect(formatNumber(1_000)).toBe('1.00k');
|
||||
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(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', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue