chore: add prettier with config and format all files
This commit is contained in:
@@ -7,34 +7,43 @@ import { resolveName, resolveKey } from '@/lib/localization';
|
||||
import type { AlertConfig } from '@/lib/types';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const inputCls = 'w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-indigo-500';
|
||||
const selectCls = 'flex-1 bg-gray-800 border border-gray-600 rounded px-2 py-1 text-sm text-white focus:outline-none';
|
||||
const inputCls =
|
||||
'w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-indigo-500';
|
||||
const selectCls =
|
||||
'flex-1 bg-gray-800 border border-gray-600 rounded px-2 py-1 text-sm text-white focus:outline-none';
|
||||
|
||||
interface AlertFormState {
|
||||
itemKey: string;
|
||||
itemKey: string;
|
||||
itemKeyIsRegex: boolean;
|
||||
combinator: string;
|
||||
signalType: 'green' | 'red';
|
||||
condition: 'above' | 'below';
|
||||
threshold: string;
|
||||
combinator: string;
|
||||
signalType: 'green' | 'red';
|
||||
condition: 'above' | 'below';
|
||||
threshold: string;
|
||||
}
|
||||
|
||||
function emptyForm(): AlertFormState {
|
||||
return { itemKey: '', itemKeyIsRegex: false, combinator: '', signalType: 'green', condition: 'below', threshold: '0' };
|
||||
return {
|
||||
itemKey: '',
|
||||
itemKeyIsRegex: false,
|
||||
combinator: '',
|
||||
signalType: 'green',
|
||||
condition: 'below',
|
||||
threshold: '0',
|
||||
};
|
||||
}
|
||||
|
||||
function alertToForm(a: AlertConfig): AlertFormState {
|
||||
return {
|
||||
itemKey: a.item_key,
|
||||
itemKey: a.item_key,
|
||||
itemKeyIsRegex: a.item_key_is_regex,
|
||||
combinator: a.combinator ?? '',
|
||||
signalType: a.signal_type,
|
||||
condition: a.condition,
|
||||
threshold: String(a.threshold),
|
||||
combinator: a.combinator ?? '',
|
||||
signalType: a.signal_type,
|
||||
condition: a.condition,
|
||||
threshold: String(a.threshold),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,48 +59,78 @@ function Tooltip({ text }: { text: string }) {
|
||||
}
|
||||
|
||||
function AlertForm({
|
||||
value, onChange, onSubmit, onCancel, submitLabel,
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitLabel,
|
||||
}: {
|
||||
value: AlertFormState;
|
||||
onChange: (s: AlertFormState) => void;
|
||||
onSubmit: () => void;
|
||||
onCancel?: () => void;
|
||||
value: AlertFormState;
|
||||
onChange: (s: AlertFormState) => void;
|
||||
onSubmit: () => void;
|
||||
onCancel?: () => void;
|
||||
submitLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
value={value.itemKey}
|
||||
onChange={e => onChange({ ...value, itemKey: e.target.value })}
|
||||
onChange={(e) => onChange({ ...value, itemKey: e.target.value })}
|
||||
placeholder={value.itemKeyIsRegex ? 'iron-.*|Iron Plate' : 'Iron Plate or item-key'}
|
||||
className={inputCls}
|
||||
/>
|
||||
<label className="flex items-center gap-1.5 text-xs text-gray-400 cursor-pointer">
|
||||
<input type="checkbox" checked={value.itemKeyIsRegex}
|
||||
onChange={e => onChange({ ...value, itemKeyIsRegex: e.target.checked })}
|
||||
className="accent-indigo-500" />
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value.itemKeyIsRegex}
|
||||
onChange={(e) => onChange({ ...value, itemKeyIsRegex: e.target.checked })}
|
||||
className="accent-indigo-500"
|
||||
/>
|
||||
Item key is regex
|
||||
</label>
|
||||
<input value={value.combinator} onChange={e => onChange({ ...value, combinator: e.target.value })}
|
||||
placeholder="combinator (empty = all)" className={inputCls} />
|
||||
<input
|
||||
value={value.combinator}
|
||||
onChange={(e) => onChange({ ...value, combinator: e.target.value })}
|
||||
placeholder="combinator (empty = all)"
|
||||
className={inputCls}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<select value={value.signalType} onChange={e => onChange({ ...value, signalType: e.target.value as 'green' | 'red' })} className={selectCls}>
|
||||
<select
|
||||
value={value.signalType}
|
||||
onChange={(e) => onChange({ ...value, signalType: e.target.value as 'green' | 'red' })}
|
||||
className={selectCls}
|
||||
>
|
||||
<option value="green">Green</option>
|
||||
<option value="red">Red</option>
|
||||
</select>
|
||||
<select value={value.condition} onChange={e => onChange({ ...value, condition: e.target.value as 'above' | 'below' })} className={selectCls}>
|
||||
<select
|
||||
value={value.condition}
|
||||
onChange={(e) => onChange({ ...value, condition: e.target.value as 'above' | 'below' })}
|
||||
className={selectCls}
|
||||
>
|
||||
<option value="below">Below</option>
|
||||
<option value="above">Above</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="number" value={value.threshold} onChange={e => onChange({ ...value, threshold: e.target.value })}
|
||||
placeholder="threshold" className={inputCls} />
|
||||
<input
|
||||
type="number"
|
||||
value={value.threshold}
|
||||
onChange={(e) => onChange({ ...value, threshold: e.target.value })}
|
||||
placeholder="threshold"
|
||||
className={inputCls}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={onSubmit} className="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white text-sm rounded py-1.5">
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white text-sm rounded py-1.5"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
{onCancel && (
|
||||
<button onClick={onCancel} className="px-3 py-1.5 text-sm text-gray-400 hover:text-white rounded hover:bg-gray-700">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-3 py-1.5 text-sm text-gray-400 hover:text-white rounded hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
@@ -102,10 +141,10 @@ function AlertForm({
|
||||
|
||||
export default function AlertPanel({ open, onClose }: Props) {
|
||||
const { triggeredAlerts, refreshAlerts, localeMap, reverseMap } = useApp();
|
||||
const [alerts, setAlerts] = useState<AlertConfig[]>([]);
|
||||
const [newForm, setNewForm] = useState<AlertFormState>(emptyForm());
|
||||
const [alerts, setAlerts] = useState<AlertConfig[]>([]);
|
||||
const [newForm, setNewForm] = useState<AlertFormState>(emptyForm());
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editForm, setEditForm] = useState<AlertFormState>(emptyForm());
|
||||
const [editForm, setEditForm] = useState<AlertFormState>(emptyForm());
|
||||
const prevTriggeredCount = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -115,8 +154,8 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
useEffect(() => {
|
||||
if (triggeredAlerts.length > prevTriggeredCount.current) {
|
||||
try {
|
||||
const ctx = new AudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
const ctx = new AudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
@@ -125,7 +164,9 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.4);
|
||||
} catch { /* AudioContext blocked */ }
|
||||
} catch {
|
||||
/* AudioContext blocked */
|
||||
}
|
||||
}
|
||||
prevTriggeredCount.current = triggeredAlerts.length;
|
||||
}, [triggeredAlerts.length]);
|
||||
@@ -139,14 +180,14 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
async function handleCreate() {
|
||||
if (!newForm.itemKey.trim()) return;
|
||||
const created = await createAlert({
|
||||
item_key: normalizeItemKey(newForm),
|
||||
item_key: normalizeItemKey(newForm),
|
||||
item_key_is_regex: newForm.itemKeyIsRegex,
|
||||
combinator: newForm.combinator.trim() || null,
|
||||
signal_type: newForm.signalType,
|
||||
condition: newForm.condition,
|
||||
threshold: parseInt(newForm.threshold, 10),
|
||||
combinator: newForm.combinator.trim() || null,
|
||||
signal_type: newForm.signalType,
|
||||
condition: newForm.condition,
|
||||
threshold: parseInt(newForm.threshold, 10),
|
||||
});
|
||||
setAlerts(a => [created, ...a]);
|
||||
setAlerts((a) => [created, ...a]);
|
||||
setNewForm(emptyForm());
|
||||
await refreshAlerts();
|
||||
}
|
||||
@@ -154,21 +195,21 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
async function handleEdit(id: string) {
|
||||
if (!editForm.itemKey.trim()) return;
|
||||
const updated = await updateAlert(id, {
|
||||
item_key: normalizeItemKey(editForm),
|
||||
item_key: normalizeItemKey(editForm),
|
||||
item_key_is_regex: editForm.itemKeyIsRegex,
|
||||
combinator: editForm.combinator.trim() || null,
|
||||
signal_type: editForm.signalType,
|
||||
condition: editForm.condition,
|
||||
threshold: parseInt(editForm.threshold, 10),
|
||||
combinator: editForm.combinator.trim() || null,
|
||||
signal_type: editForm.signalType,
|
||||
condition: editForm.condition,
|
||||
threshold: parseInt(editForm.threshold, 10),
|
||||
});
|
||||
setAlerts(a => a.map(x => x.id === id ? updated : x));
|
||||
setAlerts((a) => a.map((x) => (x.id === id ? updated : x)));
|
||||
setEditingId(null);
|
||||
await refreshAlerts();
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
await deleteAlert(id);
|
||||
setAlerts(a => a.filter(x => x.id !== id));
|
||||
setAlerts((a) => a.filter((x) => x.id !== id));
|
||||
await refreshAlerts();
|
||||
}
|
||||
|
||||
@@ -178,21 +219,29 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`fixed top-0 right-0 h-full w-80 bg-gray-900 border-l border-gray-700 shadow-xl z-40 transform transition-transform duration-200 ${open ? 'translate-x-0' : 'translate-x-full'}`}>
|
||||
<div
|
||||
className={`fixed top-0 right-0 h-full w-80 bg-gray-900 border-l border-gray-700 shadow-xl z-40 transform transition-transform duration-200 ${open ? 'translate-x-0' : 'translate-x-full'}`}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
||||
<span className="font-semibold text-white">Alerts</span>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white">✕</button>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{triggeredAlerts.length > 0 && (
|
||||
<div className="px-4 py-2 bg-red-900/40 border-b border-red-800">
|
||||
<p className="text-red-300 text-xs font-semibold mb-1">🔴 TRIGGERED ({triggeredAlerts.length})</p>
|
||||
<p className="text-red-300 text-xs font-semibold mb-1">
|
||||
🔴 TRIGGERED ({triggeredAlerts.length})
|
||||
</p>
|
||||
{triggeredAlerts.map((a, i) => (
|
||||
<div key={i} className="text-xs text-red-200 flex items-center gap-1 flex-wrap">
|
||||
<span>{resolveName(a.matched_item_key, localeMap)}</span>
|
||||
<span className="text-red-400">({a.combinator_match})</span>
|
||||
<span>[{a.signal_type}]</span>
|
||||
<span>= {a.current_value} {a.condition} {a.threshold}</span>
|
||||
<span>
|
||||
= {a.current_value} {a.condition} {a.threshold}
|
||||
</span>
|
||||
{a.item_key_is_regex && a.matched_item_key !== a.item_key && (
|
||||
<Tooltip text={`Matched by regex: /${a.item_key}/`} />
|
||||
)}
|
||||
@@ -203,12 +252,17 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
|
||||
<div className="p-4 border-b border-gray-700 space-y-2">
|
||||
<p className="text-xs text-gray-400 font-semibold uppercase">New Alert</p>
|
||||
<AlertForm value={newForm} onChange={setNewForm} onSubmit={handleCreate} submitLabel="Add Alert" />
|
||||
<AlertForm
|
||||
value={newForm}
|
||||
onChange={setNewForm}
|
||||
onSubmit={handleCreate}
|
||||
submitLabel="Add Alert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto flex-1 p-4 space-y-2">
|
||||
{alerts.length === 0 && <p className="text-gray-500 text-sm">No alerts configured.</p>}
|
||||
{alerts.map(a => (
|
||||
{alerts.map((a) => (
|
||||
<div key={a.id} className="bg-gray-800 rounded p-2 text-xs text-gray-300">
|
||||
{editingId === a.id ? (
|
||||
<AlertForm
|
||||
@@ -221,18 +275,30 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
) : (
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<span className="font-medium text-white">{resolveName(a.item_key, localeMap)}</span>
|
||||
{a.item_key_is_regex && (
|
||||
<Tooltip text={`Regex pattern: /${a.item_key}/`} />
|
||||
)}
|
||||
<span className="font-medium text-white">
|
||||
{resolveName(a.item_key, localeMap)}
|
||||
</span>
|
||||
{a.item_key_is_regex && <Tooltip text={`Regex pattern: /${a.item_key}/`} />}
|
||||
{a.combinator && <span className="text-gray-400"> @ {a.combinator}</span>}
|
||||
<br />
|
||||
<span className={a.signal_type === 'green' ? 'text-green-400' : 'text-red-400'}>[{a.signal_type}]</span>
|
||||
{' '}{a.condition} {a.threshold}
|
||||
<span className={a.signal_type === 'green' ? 'text-green-400' : 'text-red-400'}>
|
||||
[{a.signal_type}]
|
||||
</span>{' '}
|
||||
{a.condition} {a.threshold}
|
||||
</div>
|
||||
<div className="flex gap-1 ml-2 shrink-0">
|
||||
<button onClick={() => startEdit(a)} className="text-gray-500 hover:text-indigo-400">✏️</button>
|
||||
<button onClick={() => handleDelete(a.id)} className="text-gray-500 hover:text-red-400">✕</button>
|
||||
<button
|
||||
onClick={() => startEdit(a)}
|
||||
className="text-gray-500 hover:text-indigo-400"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(a.id)}
|
||||
className="text-gray-500 hover:text-red-400"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -241,4 +307,4 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user