1
0
Fork 0
tsgames/src/games/hordeseer/components/modals/manage-workers-modal.tsx

250 lines
11 KiB
TypeScript

import { useEffect, useState } from "preact/hooks";
import { Trash2, Check } from "lucide-preact";
import { useBool } from "@common/hooks/useBool";
import { useInputState } from "@common/hooks/useInputState";
import { Modal } from "@common/components/Modal";
import clsx from "clsx";
import styles from "../../assets/manage-workers-modal.module.css";
import ui from "@common/assets/ui.module.css";
import { useHordeState } from "../../contexts/state";
import { deleteWorker, fetchWorker, updateWorker, type WorkerData } from "../../utils/api";
interface Props {
open: boolean;
onClose: () => void;
}
interface WorkerEdit {
name: string;
info: string;
maintenance_mode: boolean;
}
interface WorkerCard {
data: WorkerData;
edit: WorkerEdit;
saving: boolean;
saved: boolean;
deleting: boolean;
confirmDelete: boolean;
}
export const ManageWorkersModal = ({ open, onClose }: Props) => {
const { state, dispatch } = useHordeState();
const { user, apiKey } = state;
const [workers, setWorkers] = useState<WorkerCard[]>([]);
const [error, setError] = useState<string | null>(null);
const loading = useBool(false);
useEffect(() => {
if (!open || !user || !apiKey) return;
let cancelled = false;
loading.setTrue();
setError(null);
Promise.all(user.worker_ids.map(id => fetchWorker(id, apiKey)))
.then(details => {
if (cancelled) return;
const sorted = details.sort((a, b) => {
if (a.online !== b.online) return a.online ? -1 : 1;
return a.name.localeCompare(b.name);
});
setWorkers(sorted.map(w => ({
data: w,
edit: { name: w.name, info: w.info ?? '', maintenance_mode: w.maintenance_mode },
saving: false,
saved: false,
deleting: false,
confirmDelete: false,
})));
})
.catch(() => { if (!cancelled) setError('Failed to load workers'); })
.finally(() => { if (!cancelled) loading.setFalse(); });
return () => { cancelled = true; };
}, [open, user?.worker_ids.join(',')]);
const setEdit = (id: string, patch: Partial<WorkerEdit>) => {
setWorkers(prev => prev.map(w =>
w.data.id === id
? { ...w, edit: { ...w.edit, ...patch }, saved: false }
: w
));
};
const save = async (id: string) => {
if (!apiKey) return;
setWorkers(prev => prev.map(w => w.data.id === id ? { ...w, saving: true } : w));
try {
const worker = workers.find(w => w.data.id === id)!;
const updated = await updateWorker(id, apiKey, worker.edit);
setWorkers(prev => prev.map(w =>
w.data.id === id
? { ...w, data: updated, saving: false, saved: true }
: w
));
setTimeout(() => {
setWorkers(prev => prev.map(w =>
w.data.id === id ? { ...w, saved: false } : w
));
}, 1500);
} catch {
setError('Failed to save worker');
setWorkers(prev => prev.map(w =>
w.data.id === id ? { ...w, saving: false } : w
));
}
};
const startDelete = (id: string) => {
setWorkers(prev => prev.map(w =>
w.data.id === id ? { ...w, confirmDelete: true } : w
));
};
const cancelDelete = (id: string) => {
setWorkers(prev => prev.map(w =>
w.data.id === id ? { ...w, confirmDelete: false } : w
));
};
const confirmDelete = async (id: string) => {
if (!apiKey) return;
setWorkers(prev => prev.map(w =>
w.data.id === id ? { ...w, deleting: true, confirmDelete: false } : w
));
try {
await deleteWorker(id, apiKey);
setWorkers(prev => prev.filter(w => w.data.id !== id));
dispatch({ type: 'SET_USER', user: { ...user!, worker_ids: user!.worker_ids.filter(i => i !== id) } });
} catch {
setError('Failed to delete worker');
setWorkers(prev => prev.map(w =>
w.data.id === id ? { ...w, deleting: false } : w
));
}
};
const hasChanges = workers.some(w =>
w.edit.name !== w.data.name ||
w.edit.info !== (w.data.info ?? '') ||
w.edit.maintenance_mode !== w.data.maintenance_mode
);
const footer = (
<div class={styles.footer}>
<span class={styles.footerInfo}>
{workers.length} worker{workers.length !== 1 ? 's' : ''}
{hasChanges && ' · unsaved changes'}
</span>
{hasChanges && (
<div class={styles.footerActions}>
<button class={ui.buttonPrimary} onClick={() => workers.forEach(w => { if (hasChangesForWorker(w)) save(w.data.id); })}>
Save All
</button>
</div>
)}
</div>
);
const hasChangesForWorker = (w: WorkerCard) =>
w.edit.name !== w.data.name ||
w.edit.info !== (w.data.info ?? '') ||
w.edit.maintenance_mode !== w.data.maintenance_mode;
return (
<Modal open={open && Boolean(user)} onClose={onClose} title="Manage Workers" class={styles.modal} footer={footer}>
{loading.value && <p class={styles.empty}>Loading workers</p>}
{error && <p class={styles.error}>{error}</p>}
{!loading.value && !error && workers.length === 0 && (
<p class={styles.empty}>No workers found.</p>
)}
{!loading.value && !error && workers.length > 0 && (
<div class={styles.list}>
{workers.map(w => (
<div key={w.data.id} class={styles.workerCard}>
<div class={styles.workerHeader}>
<div class={styles.workerInfo}>
<div class={styles.workerName}>{w.data.name}</div>
<div class={styles.workerModels}>
{w.data.models.slice(0, 3).join(', ')}
{w.data.models.length > 3 && ` +${w.data.models.length - 3}`}
</div>
</div>
<div class={styles.badges}>
<span class={clsx(styles.badge, w.data.online ? styles.badgeOnline : styles.badgeOffline)}>
{w.data.online ? 'online' : 'offline'}
</span>
{w.data.maintenance_mode && (
<span class={styles.badgeMaintenance}>maint</span>
)}
</div>
</div>
<div class={styles.fields}>
<div class={ui.formGroup}>
<label class={ui.label}>Name</label>
<input
class={ui.input}
value={w.edit.name}
onInput={e => setEdit(w.data.id, { name: (e.target as HTMLInputElement).value })}
/>
</div>
<div class={ui.formGroup}>
<label class={ui.label}>Info</label>
<textarea
class={ui.textarea}
value={w.edit.info}
onInput={e => setEdit(w.data.id, { info: (e.target as HTMLTextAreaElement).value })}
rows={2}
/>
</div>
</div>
<div class={styles.actions}>
<label class={styles.checkboxLabel}>
<input
type="checkbox"
checked={w.edit.maintenance_mode}
onChange={e => setEdit(w.data.id, { maintenance_mode: (e.target as HTMLInputElement).checked })}
/>
Maint.
</label>
{w.saved && (
<span class={styles.toast}>
<Check size={14} class={styles.toastIcon} />
Saved
</span>
)}
<div class={styles.spacer} />
{w.confirmDelete ? (
<div class={styles.deleteConfirm}>
<span>Really delete?</span>
<button class={ui.confirmButton} onClick={() => confirmDelete(w.data.id)} disabled={w.deleting}>
Yes
</button>
<button class={ui.cancelButton} onClick={() => cancelDelete(w.data.id)} disabled={w.deleting}>
No
</button>
</div>
) : (
<>
<button class={ui.buttonSecondary} onClick={() => save(w.data.id)} disabled={w.saving || !hasChangesForWorker(w)}>
{w.saving ? 'Saving…' : 'Save'}
</button>
<button class={styles.deleteBtn} onClick={() => startDelete(w.data.id)} disabled={w.deleting}>
<Trash2 size={16} />
Delete
</button>
</>
)}
</div>
</div>
))}
</div>
)}
</Modal>
);
};