restructure pages dir
This commit is contained in:
30
frontend/src/pages/addresses/Addresses.css
Normal file
30
frontend/src/pages/addresses/Addresses.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.address__edit-input {
|
||||
background: var(--color-border);
|
||||
border: 1px solid #666;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text);
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.address__edit-link {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6699cc;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.address__remove {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-danger-border);
|
||||
color: var(--color-danger);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-size: 0.85rem;
|
||||
margin-left: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
159
frontend/src/pages/addresses/Addresses.jsx
Normal file
159
frontend/src/pages/addresses/Addresses.jsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import AddressForm from "../../components/AddressForm";
|
||||
import { getAddresses, createAddress, deleteAddress, updateAddress } from "../../api/addresses";
|
||||
import "./Addresses.css";
|
||||
|
||||
export default function Addresses() {
|
||||
const [addresses, setAddresses] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editLabel, setEditLabel] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchAddresses() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getAddresses();
|
||||
setAddresses(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error("Failed to fetch addresses:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchAddresses();
|
||||
}, []);
|
||||
|
||||
async function handleAddressSubmit(data) {
|
||||
try {
|
||||
const newAddress = await createAddress(data);
|
||||
setAddresses((prev) => [...prev, newAddress]);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error("Failed to create address:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id, label) {
|
||||
const displayName = label || "this address";
|
||||
if (!window.confirm(`Remove "${displayName}"? This will also delete all associated alert rules.`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteAddress(id);
|
||||
setAddresses((prev) => prev.filter((a) => a.id !== id));
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error("Failed to delete address:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditStart(addr) {
|
||||
setEditingId(addr.id);
|
||||
setEditLabel(addr.label ?? "");
|
||||
}
|
||||
|
||||
async function handleEditSave(id) {
|
||||
try {
|
||||
const updated = await updateAddress(id, { label: editLabel || null });
|
||||
setAddresses((prev) => prev.map((a) => (a.id === id ? updated : a)));
|
||||
setEditingId(null);
|
||||
setEditLabel("");
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error("Failed to update address:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditCancel() {
|
||||
setEditingId(null);
|
||||
setEditLabel("");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h1>Add Addresses to Track</h1>
|
||||
|
||||
<div className="mb-xl">
|
||||
<AddressForm onSubmit={handleAddressSubmit} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Existing Tracked Addresses</h2>
|
||||
{loading && <p>Loading addresses...</p>}
|
||||
{error && <p className="text-error">Error: {error}</p>}
|
||||
{!loading && !error && addresses.length === 0 && (
|
||||
<p className="text-dimmed">
|
||||
No addresses tracked yet. Add one above to get started.
|
||||
</p>
|
||||
)}
|
||||
{addresses.length > 0 && (
|
||||
<ul className="list-unstyled">
|
||||
{addresses.map((addr, index) => (
|
||||
<li key={addr.id || index} className="list-item--card">
|
||||
<div className="flex flex--between flex--center">
|
||||
<div>
|
||||
{editingId === addr.id ? (
|
||||
<div className="flex flex--center gap-sm mb-sm">
|
||||
<input
|
||||
value={editLabel}
|
||||
onChange={(e) => setEditLabel(e.target.value)}
|
||||
placeholder="Label (optional)"
|
||||
className="address__edit-input"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleEditSave(addr.id);
|
||||
if (e.key === "Escape") handleEditCancel();
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleEditSave(addr.id)}
|
||||
className="btn btn--primary btn--sm"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEditCancel}
|
||||
className="btn btn--ghost btn--sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex--center gap-sm mb-sm">
|
||||
<span className="text-bold">
|
||||
{addr.label || "Unlabeled"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleEditStart(addr)}
|
||||
className="address__edit-link"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-mono text-sm text-muted">
|
||||
{addr.address}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDelete(addr.id, addr.label)}
|
||||
className="address__remove"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user