fmt: apply prettier to frontend JS/TS/CSS/JSON files
Initial prettier pass with tabWidth=4.
This commit is contained in:
5767
frontend/package-lock.json
generated
5767
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,56 +7,71 @@ import Alerts from "./pages/Alerts";
|
|||||||
import AlertHistory from "./pages/AlertHistory";
|
import AlertHistory from "./pages/AlertHistory";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { currentUser, logout } = useAuth();
|
const { currentUser, logout } = useAuth();
|
||||||
|
|
||||||
// Show login/signup routes if not authenticated
|
// Show login/signup routes if not authenticated
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/signup" element={<Signup />} />
|
||||||
|
<Route path="*" element={<Navigate to="/login" />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show main app if authenticated
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<div style={{ padding: "1rem" }}>
|
||||||
<Route path="/login" element={<Login />} />
|
<nav
|
||||||
<Route path="/signup" element={<Signup />} />
|
style={{
|
||||||
<Route path="*" element={<Navigate to="/login" />} />
|
marginBottom: "1rem",
|
||||||
</Routes>
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Link to="/addresses">Addresses</Link>
|
||||||
|
{" | "}
|
||||||
|
<Link to="/alerts">Alerts</Link>
|
||||||
|
{" | "}
|
||||||
|
<Link to="/history">History</Link>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: "0.9rem", color: "#999" }}>
|
||||||
|
{currentUser.email}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
style={{
|
||||||
|
padding: "0.5rem 1rem",
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
backgroundColor: "#333",
|
||||||
|
color: "white",
|
||||||
|
border: "1px solid #555",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Addresses />} />
|
||||||
|
<Route path="/addresses" element={<Addresses />} />
|
||||||
|
<Route path="/alerts" element={<Alerts />} />
|
||||||
|
<Route path="/history" element={<AlertHistory />} />
|
||||||
|
<Route path="*" element={<Navigate to="/addresses" />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Show main app if authenticated
|
|
||||||
return (
|
|
||||||
<div style={{ padding: "1rem" }}>
|
|
||||||
<nav style={{ marginBottom: "1rem", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
||||||
<div>
|
|
||||||
<Link to="/addresses">Addresses</Link>{" | "}
|
|
||||||
<Link to="/alerts">Alerts</Link>{" | "}
|
|
||||||
<Link to="/history">History</Link>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
|
||||||
<span style={{ fontSize: "0.9rem", color: "#999" }}>
|
|
||||||
{currentUser.email}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
style={{
|
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
backgroundColor: "#333",
|
|
||||||
color: "white",
|
|
||||||
border: "1px solid #555",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Addresses />} />
|
|
||||||
<Route path="/addresses" element={<Addresses />} />
|
|
||||||
<Route path="/alerts" element={<Alerts />} />
|
|
||||||
<Route path="/history" element={<AlertHistory />} />
|
|
||||||
<Route path="*" element={<Navigate to="/addresses" />} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// API client for address management
|
// API client for address management
|
||||||
|
|
||||||
import { getAuthHeaders, getAuthHeadersSimple } from './authHeaders';
|
import { getAuthHeaders, getAuthHeadersSimple } from "./authHeaders";
|
||||||
import { API_BASE } from './config';
|
import { API_BASE } from "./config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new blockchain address to monitor
|
* Create a new blockchain address to monitor
|
||||||
@@ -11,35 +11,37 @@ import { API_BASE } from './config';
|
|||||||
* @returns {Promise<Object>} Created address with id and metadata
|
* @returns {Promise<Object>} Created address with id and metadata
|
||||||
*/
|
*/
|
||||||
export async function createAddress(data) {
|
export async function createAddress(data) {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeaders();
|
const headers = await getAuthHeaders();
|
||||||
const response = await fetch(`${API_BASE}/addresses`, {
|
const response = await fetch(`${API_BASE}/addresses`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Try to parse error response, but handle if it's not JSON
|
// Try to parse error response, but handle if it's not JSON
|
||||||
let errorMessage = 'Failed to create address';
|
let errorMessage = "Failed to create address";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
// If JSON parse fails, use status text
|
// If JSON parse fails, use status text
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle network errors
|
// Handle network errors
|
||||||
if (error.message.includes('fetch')) {
|
if (error.message.includes("fetch")) {
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,33 +49,35 @@ export async function createAddress(data) {
|
|||||||
* @returns {Promise<Array>} List of all addresses
|
* @returns {Promise<Array>} List of all addresses
|
||||||
*/
|
*/
|
||||||
export async function getAddresses() {
|
export async function getAddresses() {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeadersSimple();
|
const headers = await getAuthHeadersSimple();
|
||||||
const response = await fetch(`${API_BASE}/addresses`, {
|
const response = await fetch(`${API_BASE}/addresses`, {
|
||||||
headers: headers
|
headers: headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Try to parse error response, but handle if it's not JSON
|
// Try to parse error response, but handle if it's not JSON
|
||||||
let errorMessage = 'Failed to fetch addresses';
|
let errorMessage = "Failed to fetch addresses";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
// If JSON parse fails, use status text
|
// If JSON parse fails, use status text
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle network errors or JSON parse errors
|
// Handle network errors or JSON parse errors
|
||||||
if (error.message.includes('fetch')) {
|
if (error.message.includes("fetch")) {
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,31 +86,32 @@ export async function getAddresses() {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function deleteAddress(addressId) {
|
export async function deleteAddress(addressId) {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeadersSimple();
|
const headers = await getAuthHeadersSimple();
|
||||||
const response = await fetch(`${API_BASE}/addresses/${addressId}`, {
|
const response = await fetch(`${API_BASE}/addresses/${addressId}`, {
|
||||||
method: 'DELETE',
|
method: "DELETE",
|
||||||
headers: headers
|
headers: headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok && response.status !== 204) {
|
if (!response.ok && response.status !== 204) {
|
||||||
// Try to parse error response, but handle if it's not JSON
|
// Try to parse error response, but handle if it's not JSON
|
||||||
let errorMessage = 'Failed to delete address';
|
let errorMessage = "Failed to delete address";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
// If JSON parse fails, use status text
|
// If JSON parse fails, use status text
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Handle network errors
|
||||||
|
if (error.message.includes("fetch")) {
|
||||||
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
// Handle network errors
|
|
||||||
if (error.message.includes('fetch')) {
|
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
// API client for alert event history
|
// API client for alert event history
|
||||||
|
|
||||||
import { getAuthHeadersSimple } from './authHeaders';
|
import { getAuthHeadersSimple } from "./authHeaders";
|
||||||
import { API_BASE } from './config';
|
import { API_BASE } from "./config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all alert events (history)
|
* Get all alert events (history)
|
||||||
* @returns {Promise<Array>} List of alert events, sorted by most recent first
|
* @returns {Promise<Array>} List of alert events, sorted by most recent first
|
||||||
*/
|
*/
|
||||||
export async function getAlertEvents() {
|
export async function getAlertEvents() {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeadersSimple();
|
const headers = await getAuthHeadersSimple();
|
||||||
const response = await fetch(`${API_BASE}/alert-events`, {
|
const response = await fetch(`${API_BASE}/alert-events`, {
|
||||||
headers: headers
|
headers: headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorMessage = 'Failed to fetch alert events';
|
let errorMessage = "Failed to fetch alert events";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('fetch')) {
|
if (error.message.includes("fetch")) {
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
// API client for alert rule management
|
// API client for alert rule management
|
||||||
|
|
||||||
import { getAuthHeaders, getAuthHeadersSimple } from './authHeaders';
|
import { getAuthHeaders, getAuthHeadersSimple } from "./authHeaders";
|
||||||
import { API_BASE } from './config';
|
import { API_BASE } from "./config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alert types supported by the system
|
* Alert types supported by the system
|
||||||
*/
|
*/
|
||||||
export const ALERT_TYPES = {
|
export const ALERT_TYPES = {
|
||||||
INCOMING_TX: 'incoming_tx',
|
INCOMING_TX: "incoming_tx",
|
||||||
OUTGOING_TX: 'outgoing_tx',
|
OUTGOING_TX: "outgoing_tx",
|
||||||
LARGE_TRANSFER: 'large_transfer',
|
LARGE_TRANSFER: "large_transfer",
|
||||||
BALANCE_BELOW: 'balance_below'
|
BALANCE_BELOW: "balance_below",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,32 +22,37 @@ export const ALERT_TYPES = {
|
|||||||
* @returns {Promise<Object>} Created alert rule
|
* @returns {Promise<Object>} Created alert rule
|
||||||
*/
|
*/
|
||||||
export async function createAlert(addressId, data) {
|
export async function createAlert(addressId, data) {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeaders();
|
const headers = await getAuthHeaders();
|
||||||
const response = await fetch(`${API_BASE}/addresses/${addressId}/alerts`, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
`${API_BASE}/addresses/${addressId}/alerts`,
|
||||||
headers: headers,
|
{
|
||||||
body: JSON.stringify(data)
|
method: "POST",
|
||||||
});
|
headers: headers,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorMessage = 'Failed to create alert rule';
|
let errorMessage = "Failed to create alert rule";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('fetch')) {
|
if (error.message.includes("fetch")) {
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,30 +61,35 @@ export async function createAlert(addressId, data) {
|
|||||||
* @returns {Promise<Array>} List of alert rules
|
* @returns {Promise<Array>} List of alert rules
|
||||||
*/
|
*/
|
||||||
export async function getAlerts(addressId) {
|
export async function getAlerts(addressId) {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeadersSimple();
|
const headers = await getAuthHeadersSimple();
|
||||||
const response = await fetch(`${API_BASE}/addresses/${addressId}/alerts`, {
|
const response = await fetch(
|
||||||
headers: headers
|
`${API_BASE}/addresses/${addressId}/alerts`,
|
||||||
});
|
{
|
||||||
|
headers: headers,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorMessage = 'Failed to fetch alert rules';
|
let errorMessage = "Failed to fetch alert rules";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('fetch')) {
|
if (error.message.includes("fetch")) {
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,32 +99,34 @@ export async function getAlerts(addressId) {
|
|||||||
* @returns {Promise<Object>} Updated alert rule
|
* @returns {Promise<Object>} Updated alert rule
|
||||||
*/
|
*/
|
||||||
export async function updateAlertStatus(alertId, enabled) {
|
export async function updateAlertStatus(alertId, enabled) {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeaders();
|
const headers = await getAuthHeaders();
|
||||||
const response = await fetch(`${API_BASE}/alerts/${alertId}`, {
|
const response = await fetch(`${API_BASE}/alerts/${alertId}`, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: JSON.stringify({ enabled })
|
body: JSON.stringify({ enabled }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorMessage = 'Failed to update alert rule';
|
let errorMessage = "Failed to update alert rule";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('fetch')) {
|
if (error.message.includes("fetch")) {
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,27 +135,29 @@ export async function updateAlertStatus(alertId, enabled) {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function deleteAlert(alertId) {
|
export async function deleteAlert(alertId) {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeadersSimple();
|
const headers = await getAuthHeadersSimple();
|
||||||
const response = await fetch(`${API_BASE}/alerts/${alertId}`, {
|
const response = await fetch(`${API_BASE}/alerts/${alertId}`, {
|
||||||
method: 'DELETE',
|
method: "DELETE",
|
||||||
headers: headers
|
headers: headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok && response.status !== 204) {
|
if (!response.ok && response.status !== 204) {
|
||||||
let errorMessage = 'Failed to delete alert rule';
|
let errorMessage = "Failed to delete alert rule";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes("fetch")) {
|
||||||
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
if (error.message.includes('fetch')) {
|
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,26 +5,26 @@
|
|||||||
* Includes Firebase ID token in Authorization header
|
* Includes Firebase ID token in Authorization header
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { auth } from '../firebase/config';
|
import { auth } from "../firebase/config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get headers with authentication token
|
* Get headers with authentication token
|
||||||
* @returns {Promise<Object>} Headers object with Authorization
|
* @returns {Promise<Object>} Headers object with Authorization
|
||||||
*/
|
*/
|
||||||
export async function getAuthHeaders() {
|
export async function getAuthHeaders() {
|
||||||
const currentUser = auth.currentUser;
|
const currentUser = auth.currentUser;
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
throw new Error('No authenticated user');
|
throw new Error("No authenticated user");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Firebase ID token
|
// Get Firebase ID token
|
||||||
const token = await currentUser.getIdToken();
|
const token = await currentUser.getIdToken();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
'Authorization': `Bearer ${token}`
|
Authorization: `Bearer ${token}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,16 +32,15 @@ export async function getAuthHeaders() {
|
|||||||
* @returns {Promise<Object>} Headers object with Authorization
|
* @returns {Promise<Object>} Headers object with Authorization
|
||||||
*/
|
*/
|
||||||
export async function getAuthHeadersSimple() {
|
export async function getAuthHeadersSimple() {
|
||||||
const currentUser = auth.currentUser;
|
const currentUser = auth.currentUser;
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
throw new Error('No authenticated user');
|
throw new Error("No authenticated user");
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await currentUser.getIdToken();
|
const token = await currentUser.getIdToken();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'Authorization': `Bearer ${token}`
|
Authorization: `Bearer ${token}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const API_BASE = import.meta.env.VITE_API_BASE || '/v1';
|
export const API_BASE = import.meta.env.VITE_API_BASE || "/v1";
|
||||||
|
|||||||
@@ -1,37 +1,39 @@
|
|||||||
// API client for notification configuration
|
// API client for notification configuration
|
||||||
|
|
||||||
import { getAuthHeaders } from './authHeaders';
|
import { getAuthHeaders } from "./authHeaders";
|
||||||
import { API_BASE } from './config';
|
import { API_BASE } from "./config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get notification configuration for current user
|
* Get notification configuration for current user
|
||||||
* @returns {Promise<Object>} Notification config
|
* @returns {Promise<Object>} Notification config
|
||||||
*/
|
*/
|
||||||
export async function getNotificationConfig() {
|
export async function getNotificationConfig() {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeaders();
|
const headers = await getAuthHeaders();
|
||||||
const response = await fetch(`${API_BASE}/notification-config`, {
|
const response = await fetch(`${API_BASE}/notification-config`, {
|
||||||
headers: headers
|
headers: headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorMessage = 'Failed to fetch notification config';
|
let errorMessage = "Failed to fetch notification config";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('fetch')) {
|
if (error.message.includes("fetch")) {
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,32 +46,34 @@ export async function getNotificationConfig() {
|
|||||||
* @returns {Promise<Object>} Updated config
|
* @returns {Promise<Object>} Updated config
|
||||||
*/
|
*/
|
||||||
export async function updateNotificationConfig(config) {
|
export async function updateNotificationConfig(config) {
|
||||||
try {
|
try {
|
||||||
const headers = await getAuthHeaders();
|
const headers = await getAuthHeaders();
|
||||||
const response = await fetch(`${API_BASE}/notification-config`, {
|
const response = await fetch(`${API_BASE}/notification-config`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: JSON.stringify(config)
|
body: JSON.stringify(config),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorMessage = 'Failed to update notification config';
|
let errorMessage = "Failed to update notification config";
|
||||||
try {
|
try {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
errorMessage = error.message || errorMessage;
|
errorMessage = error.message || errorMessage;
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||||
}
|
}
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('fetch')) {
|
if (error.message.includes("fetch")) {
|
||||||
throw new Error('Cannot connect to server. Is the backend running?');
|
throw new Error(
|
||||||
|
"Cannot connect to server. Is the backend running?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,21 +83,21 @@ export async function updateNotificationConfig(config) {
|
|||||||
* @returns {Promise<boolean>} True if test successful
|
* @returns {Promise<boolean>} True if test successful
|
||||||
*/
|
*/
|
||||||
export async function testDiscordWebhook(webhookUrl) {
|
export async function testDiscordWebhook(webhookUrl) {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
content: 'Koin Ping test notification - Your Discord webhook is configured correctly!',
|
content:
|
||||||
};
|
"Koin Ping test notification - Your Discord webhook is configured correctly!",
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch(webhookUrl, {
|
const response = await fetch(webhookUrl, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Discord webhook test failed:', error);
|
console.error("Discord webhook test failed:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
// API client for system status
|
// API client for system status
|
||||||
|
|
||||||
import { API_BASE } from './config';
|
import { API_BASE } from "./config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get system status including latest processed block and health
|
* Get system status including latest processed block and health
|
||||||
* @returns {Promise<Object>} System status
|
* @returns {Promise<Object>} System status
|
||||||
*/
|
*/
|
||||||
export async function getSystemStatus() {
|
export async function getSystemStatus() {
|
||||||
const response = await fetch(`${API_BASE}/status`);
|
const response = await fetch(`${API_BASE}/status`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
throw new Error(error.message || 'Failed to fetch system status');
|
throw new Error(error.message || "Failed to fetch system status");
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,43 +3,43 @@ import Input from "./Input";
|
|||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
|
|
||||||
export default function AddressForm({ onSubmit }) {
|
export default function AddressForm({ onSubmit }) {
|
||||||
const [address, setAddress] = useState("");
|
const [address, setAddress] = useState("");
|
||||||
const [label, setLabel] = useState("");
|
const [label, setLabel] = useState("");
|
||||||
|
|
||||||
const canSubmit = address.trim().length > 0;
|
const canSubmit = address.trim().length > 0;
|
||||||
|
|
||||||
function handleSubmit(e) {
|
function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!canSubmit) return;
|
if (!canSubmit) return;
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
address: address.trim(),
|
address: address.trim(),
|
||||||
label: label.trim(),
|
label: label.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
setAddress("");
|
setAddress("");
|
||||||
setLabel("");
|
setLabel("");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Input
|
<Input
|
||||||
label="Blockchain Address"
|
label="Blockchain Address"
|
||||||
value={address}
|
value={address}
|
||||||
onChange={setAddress}
|
onChange={setAddress}
|
||||||
placeholder="0x..."
|
placeholder="0x..."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Label (optional)"
|
label="Label (optional)"
|
||||||
value={label}
|
value={label}
|
||||||
onChange={setLabel}
|
onChange={setLabel}
|
||||||
placeholder="Treasury, Cold Wallet, etc."
|
placeholder="Treasury, Cold Wallet, etc."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" disabled={!canSubmit}>
|
<Button type="submit" disabled={!canSubmit}>
|
||||||
Add Address
|
Add Address
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,69 +3,69 @@ import Input from "./Input";
|
|||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
|
|
||||||
const ALERT_TYPES = [
|
const ALERT_TYPES = [
|
||||||
{ value: "incoming_tx", label: "Incoming transaction" },
|
{ value: "incoming_tx", label: "Incoming transaction" },
|
||||||
{ value: "outgoing_tx", label: "Outgoing transaction" },
|
{ value: "outgoing_tx", label: "Outgoing transaction" },
|
||||||
{ value: "large_transfer", label: "Large transfer" },
|
{ value: "large_transfer", label: "Large transfer" },
|
||||||
{ value: "balance_below", label: "Balance below threshold" },
|
{ value: "balance_below", label: "Balance below threshold" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AlertForm({ onSubmit }) {
|
export default function AlertForm({ onSubmit }) {
|
||||||
const [type, setType] = useState("incoming_tx");
|
const [type, setType] = useState("incoming_tx");
|
||||||
const [threshold, setThreshold] = useState("");
|
const [threshold, setThreshold] = useState("");
|
||||||
|
|
||||||
const needsThreshold =
|
const needsThreshold =
|
||||||
type === "large_transfer" || type === "balance_below";
|
type === "large_transfer" || type === "balance_below";
|
||||||
|
|
||||||
const canSubmit = needsThreshold
|
const canSubmit = needsThreshold
|
||||||
? threshold.trim() !== "" &&
|
? threshold.trim() !== "" &&
|
||||||
!isNaN(Number(threshold)) &&
|
!isNaN(Number(threshold)) &&
|
||||||
Number(threshold) > 0
|
Number(threshold) > 0
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
function handleSubmit(e) {
|
function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!canSubmit) return;
|
if (!canSubmit) return;
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
type,
|
type,
|
||||||
threshold: needsThreshold ? Number(threshold) : undefined,
|
threshold: needsThreshold ? Number(threshold) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
setThreshold("");
|
setThreshold("");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div style={{ marginBottom: "1rem" }}>
|
<div style={{ marginBottom: "1rem" }}>
|
||||||
{ALERT_TYPES.map((opt) => (
|
{ALERT_TYPES.map((opt) => (
|
||||||
<label key={opt.value} style={{ display: "block" }}>
|
<label key={opt.value} style={{ display: "block" }}>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="alertType"
|
name="alertType"
|
||||||
value={opt.value}
|
value={opt.value}
|
||||||
checked={type === opt.value}
|
checked={type === opt.value}
|
||||||
onChange={() => setType(opt.value)}
|
onChange={() => setType(opt.value)}
|
||||||
/>
|
/>{" "}
|
||||||
{" "}{opt.label}
|
{opt.label}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{needsThreshold && (
|
{needsThreshold && (
|
||||||
<Input
|
<Input
|
||||||
label="Amount (ETH)"
|
label="Amount (ETH)"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.000001"
|
step="0.000001"
|
||||||
min="0"
|
min="0"
|
||||||
value={threshold}
|
value={threshold}
|
||||||
onChange={setThreshold}
|
onChange={setThreshold}
|
||||||
placeholder="e.g. 10"
|
placeholder="e.g. 10"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="submit" disabled={!canSubmit}>
|
<Button type="submit" disabled={!canSubmit}>
|
||||||
Create Alert
|
Create Alert
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,20 @@ export default function Button({
|
|||||||
onClick,
|
onClick,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
type = "button",
|
type = "button",
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{
|
style={{
|
||||||
padding: "0.5rem 1rem",
|
padding: "0.5rem 1rem",
|
||||||
fontSize: "1rem",
|
fontSize: "1rem",
|
||||||
cursor: disabled ? "not-allowed" : "pointer",
|
cursor: disabled ? "not-allowed" : "pointer",
|
||||||
opacity: disabled ? 0.6 : 1,
|
opacity: disabled ? 0.6 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,27 +7,26 @@ export default function Input({
|
|||||||
step,
|
step,
|
||||||
min,
|
min,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<label style={{ display: "block", marginBottom: "1rem" }}>
|
<label style={{ display: "block", marginBottom: "1rem" }}>
|
||||||
<div style={{ marginBottom: "0.25rem", fontSize: "0.9rem" }}>
|
<div style={{ marginBottom: "0.25rem", fontSize: "0.9rem" }}>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
step={step}
|
step={step}
|
||||||
min={min}
|
min={min}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
padding: "0.5rem",
|
padding: "0.5rem",
|
||||||
fontSize: "1rem",
|
fontSize: "1rem",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
* Provides authentication state and methods throughout the app
|
* Provides authentication state and methods throughout the app
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
createUserWithEmailAndPassword,
|
createUserWithEmailAndPassword,
|
||||||
signInWithEmailAndPassword,
|
signInWithEmailAndPassword,
|
||||||
signOut,
|
signOut,
|
||||||
onAuthStateChanged
|
onAuthStateChanged,
|
||||||
} from 'firebase/auth';
|
} from "firebase/auth";
|
||||||
import { auth } from '../firebase/config';
|
import { auth } from "../firebase/config";
|
||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
@@ -20,88 +20,95 @@ const AuthContext = createContext();
|
|||||||
* @returns {Object} Auth context value
|
* @returns {Object} Auth context value
|
||||||
*/
|
*/
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useAuth must be used within AuthProvider');
|
throw new Error("useAuth must be used within AuthProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AuthProvider - Wraps app and provides auth state
|
* AuthProvider - Wraps app and provides auth state
|
||||||
*/
|
*/
|
||||||
export function AuthProvider({ children }) {
|
export function AuthProvider({ children }) {
|
||||||
const [currentUser, setCurrentUser] = useState(null);
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign up with email and password
|
* Sign up with email and password
|
||||||
*/
|
*/
|
||||||
async function signup(email, password) {
|
async function signup(email, password) {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
const result = await createUserWithEmailAndPassword(auth, email, password);
|
const result = await createUserWithEmailAndPassword(
|
||||||
return result.user;
|
auth,
|
||||||
} catch (err) {
|
email,
|
||||||
setError(err.message);
|
password,
|
||||||
throw err;
|
);
|
||||||
|
return result.user;
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log in with email and password
|
* Log in with email and password
|
||||||
*/
|
*/
|
||||||
async function login(email, password) {
|
async function login(email, password) {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
const result = await signInWithEmailAndPassword(auth, email, password);
|
const result = await signInWithEmailAndPassword(
|
||||||
return result.user;
|
auth,
|
||||||
} catch (err) {
|
email,
|
||||||
setError(err.message);
|
password,
|
||||||
throw err;
|
);
|
||||||
|
return result.user;
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log out current user
|
* Log out current user
|
||||||
*/
|
*/
|
||||||
async function logout() {
|
async function logout() {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
await signOut(auth);
|
await signOut(auth);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
throw err;
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for auth state changes
|
* Listen for auth state changes
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
||||||
setCurrentUser(user);
|
setCurrentUser(user);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup subscription
|
// Cleanup subscription
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
currentUser,
|
currentUser,
|
||||||
signup,
|
signup,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
error,
|
error,
|
||||||
loading
|
loading,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={value}>
|
<AuthContext.Provider value={value}>
|
||||||
{!loading && children}
|
{!loading && children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
import { initializeApp } from "firebase/app";
|
||||||
import { initializeApp } from 'firebase/app';
|
import { getAuth } from "firebase/auth";
|
||||||
import { getAuth } from 'firebase/auth';
|
|
||||||
import {
|
import {
|
||||||
firebaseApiKey,
|
firebaseApiKey,
|
||||||
firebaseAuthDomain,
|
firebaseAuthDomain,
|
||||||
@@ -8,9 +7,8 @@ import {
|
|||||||
firebaseStorageBucket,
|
firebaseStorageBucket,
|
||||||
firebaseMessagingSenderId,
|
firebaseMessagingSenderId,
|
||||||
firebaseAppId,
|
firebaseAppId,
|
||||||
firebaseMeasurementId
|
firebaseMeasurementId,
|
||||||
} from '../secrets.js';
|
} from "../secrets.js";
|
||||||
|
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: firebaseApiKey,
|
apiKey: firebaseApiKey,
|
||||||
@@ -19,12 +17,11 @@ const firebaseConfig = {
|
|||||||
storageBucket: firebaseStorageBucket,
|
storageBucket: firebaseStorageBucket,
|
||||||
messagingSenderId: firebaseMessagingSenderId,
|
messagingSenderId: firebaseMessagingSenderId,
|
||||||
appId: firebaseAppId,
|
appId: firebaseAppId,
|
||||||
measurementId: firebaseMeasurementId
|
measurementId: firebaseMeasurementId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const app = initializeApp(firebaseConfig);
|
const app = initializeApp(firebaseConfig);
|
||||||
|
|
||||||
export const auth = getAuth(app);
|
export const auth = getAuth(app);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { AuthProvider } from "./contexts/AuthContext";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<App />
|
<App />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,81 +3,91 @@ import AddressForm from "../components/AddressForm";
|
|||||||
import { getAddresses, createAddress } from "../api/addresses";
|
import { getAddresses, createAddress } from "../api/addresses";
|
||||||
|
|
||||||
export default function Addresses() {
|
export default function Addresses() {
|
||||||
const [addresses, setAddresses] = useState([]);
|
const [addresses, setAddresses] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
// Load addresses on mount
|
// Load addresses on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchAddresses() {
|
async function fetchAddresses() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await getAddresses();
|
const data = await getAddresses();
|
||||||
setAddresses(data);
|
setAddresses(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
console.error("Failed to fetch addresses:", err);
|
console.error("Failed to fetch addresses:", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAddresses();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle new address submission
|
||||||
|
async function handleAddressSubmit(data) {
|
||||||
|
try {
|
||||||
|
const newAddress = await createAddress(data);
|
||||||
|
// Append new address to state
|
||||||
|
setAddresses((prev) => [...prev, newAddress]);
|
||||||
|
setError(null); // Clear any previous errors
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
console.error("Failed to create address:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAddresses();
|
return (
|
||||||
}, []);
|
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
|
||||||
|
<h1>Tracked Addresses</h1>
|
||||||
|
|
||||||
// Handle new address submission
|
<div style={{ marginBottom: "2rem" }}>
|
||||||
async function handleAddressSubmit(data) {
|
<AddressForm onSubmit={handleAddressSubmit} />
|
||||||
try {
|
</div>
|
||||||
const newAddress = await createAddress(data);
|
|
||||||
// Append new address to state
|
|
||||||
setAddresses((prev) => [...prev, newAddress]);
|
|
||||||
setError(null); // Clear any previous errors
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
console.error("Failed to create address:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<div>
|
||||||
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
|
<h2>Existing Addresses</h2>
|
||||||
<h1>Tracked Addresses</h1>
|
{loading && <p>Loading addresses...</p>}
|
||||||
|
{error && <p style={{ color: "red" }}>Error: {error}</p>}
|
||||||
<div style={{ marginBottom: "2rem" }}>
|
{!loading && !error && addresses.length === 0 && (
|
||||||
<AddressForm onSubmit={handleAddressSubmit} />
|
<p style={{ color: "#666" }}>
|
||||||
</div>
|
No addresses tracked yet. Add one above to get started.
|
||||||
|
</p>
|
||||||
<div>
|
)}
|
||||||
<h2>Existing Addresses</h2>
|
{addresses.length > 0 && (
|
||||||
{loading && <p>Loading addresses...</p>}
|
<ul style={{ listStyle: "none", padding: 0 }}>
|
||||||
{error && <p style={{ color: "red" }}>Error: {error}</p>}
|
{addresses.map((addr, index) => (
|
||||||
{!loading && !error && addresses.length === 0 && (
|
<li
|
||||||
<p style={{ color: "#666" }}>
|
key={addr.id || index}
|
||||||
No addresses tracked yet. Add one above to get started.
|
style={{
|
||||||
</p>
|
padding: "1rem",
|
||||||
)}
|
marginBottom: "0.5rem",
|
||||||
{addresses.length > 0 && (
|
border: "1px solid #ddd",
|
||||||
<ul style={{ listStyle: "none", padding: 0 }}>
|
borderRadius: "4px",
|
||||||
{addresses.map((addr, index) => (
|
}}
|
||||||
<li
|
>
|
||||||
key={addr.id || index}
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "1rem",
|
fontWeight: "bold",
|
||||||
marginBottom: "0.5rem",
|
marginBottom: "0.25rem",
|
||||||
border: "1px solid #ddd",
|
}}
|
||||||
borderRadius: "4px",
|
>
|
||||||
}}
|
{addr.label || "Unlabeled"}
|
||||||
>
|
</div>
|
||||||
<div style={{ fontWeight: "bold", marginBottom: "0.25rem" }}>
|
<div
|
||||||
{addr.label || "Unlabeled"}
|
style={{
|
||||||
</div>
|
fontFamily: "monospace",
|
||||||
<div style={{ fontFamily: "monospace", fontSize: "0.9rem" }}>
|
fontSize: "0.9rem",
|
||||||
{addr.address}
|
}}
|
||||||
</div>
|
>
|
||||||
</li>
|
{addr.address}
|
||||||
))}
|
</div>
|
||||||
</ul>
|
</li>
|
||||||
)}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
)}
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,73 +2,83 @@ import { useState, useEffect } from "react";
|
|||||||
import { getAlertEvents } from "../api/alertEvents";
|
import { getAlertEvents } from "../api/alertEvents";
|
||||||
|
|
||||||
export default function AlertHistory() {
|
export default function AlertHistory() {
|
||||||
const [alertEvents, setAlertEvents] = useState([]);
|
const [alertEvents, setAlertEvents] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
// Fetch alert events on mount
|
// Fetch alert events on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchAlertEvents() {
|
async function fetchAlertEvents() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await getAlertEvents();
|
const data = await getAlertEvents();
|
||||||
setAlertEvents(data);
|
setAlertEvents(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
console.error("Failed to fetch alert events:", err);
|
console.error("Failed to fetch alert events:", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAlertEvents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div style={{ padding: "2rem" }}>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAlertEvents();
|
if (error) {
|
||||||
}, []);
|
return (
|
||||||
|
<div style={{ padding: "2rem", color: "red" }}>Error: {error}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
return (
|
||||||
return <div style={{ padding: "2rem" }}>Loading...</div>;
|
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
|
||||||
}
|
<h1>Recent Alerts</h1>
|
||||||
|
|
||||||
if (error) {
|
{alertEvents.length === 0 ? (
|
||||||
return <div style={{ padding: "2rem", color: "red" }}>Error: {error}</div>;
|
<p style={{ color: "#666" }}>No alerts yet</p>
|
||||||
}
|
) : (
|
||||||
|
<ul style={{ listStyle: "none", padding: 0 }}>
|
||||||
return (
|
{alertEvents.map((event) => (
|
||||||
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
|
<li
|
||||||
<h1>Recent Alerts</h1>
|
key={event.id}
|
||||||
|
style={{
|
||||||
{alertEvents.length === 0 ? (
|
padding: "1rem",
|
||||||
<p style={{ color: "#666" }}>No alerts yet</p>
|
marginBottom: "0.75rem",
|
||||||
) : (
|
border: "1px solid #ddd",
|
||||||
<ul style={{ listStyle: "none", padding: 0 }}>
|
borderRadius: "4px",
|
||||||
{alertEvents.map((event) => (
|
}}
|
||||||
<li
|
>
|
||||||
key={event.id}
|
<div style={{ marginBottom: "0.5rem" }}>
|
||||||
style={{
|
{event.message}
|
||||||
padding: "1rem",
|
</div>
|
||||||
marginBottom: "0.75rem",
|
{event.address_label && (
|
||||||
border: "1px solid #ddd",
|
<div
|
||||||
borderRadius: "4px",
|
style={{
|
||||||
}}
|
fontSize: "0.9rem",
|
||||||
>
|
color: "#666",
|
||||||
<div style={{ marginBottom: "0.5rem" }}>{event.message}</div>
|
marginBottom: "0.25rem",
|
||||||
{event.address_label && (
|
}}
|
||||||
<div style={{ fontSize: "0.9rem", color: "#666", marginBottom: "0.25rem" }}>
|
>
|
||||||
Address: {event.address_label}
|
Address: {event.address_label}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<small style={{ color: "#999" }}>
|
<small style={{ color: "#999" }}>
|
||||||
{formatTimestamp(event.timestamp)}
|
{formatTimestamp(event.timestamp)}
|
||||||
</small>
|
</small>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to format timestamp for display
|
// Helper to format timestamp for display
|
||||||
function formatTimestamp(timestamp) {
|
function formatTimestamp(timestamp) {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
return date.toLocaleString();
|
return date.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,140 +4,143 @@
|
|||||||
* Allows existing users to sign in with email and password
|
* Allows existing users to sign in with email and password
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
setError('Please fill in all fields');
|
setError("Please fill in all fields");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
await login(email, password);
|
||||||
|
navigate("/addresses"); // Redirect to main app
|
||||||
|
} catch (err) {
|
||||||
|
setError("Failed to log in: " + err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return (
|
||||||
setError('');
|
<div
|
||||||
setLoading(true);
|
|
||||||
await login(email, password);
|
|
||||||
navigate('/addresses'); // Redirect to main app
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to log in: ' + err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
maxWidth: '400px',
|
|
||||||
margin: '4rem auto',
|
|
||||||
padding: '2rem',
|
|
||||||
border: '1px solid #333',
|
|
||||||
borderRadius: '8px'
|
|
||||||
}}>
|
|
||||||
<h1 style={{ marginBottom: '2rem', textAlign: 'center' }}>
|
|
||||||
Koin Ping - Login
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div style={{
|
|
||||||
padding: '0.75rem',
|
|
||||||
marginBottom: '1rem',
|
|
||||||
backgroundColor: '#ff000020',
|
|
||||||
border: '1px solid #ff0000',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: '#ff6666'
|
|
||||||
}}>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
maxWidth: "400px",
|
||||||
padding: '0.5rem',
|
margin: "4rem auto",
|
||||||
fontSize: '1rem',
|
padding: "2rem",
|
||||||
backgroundColor: '#1a1a1a',
|
border: "1px solid #333",
|
||||||
border: '1px solid #444',
|
borderRadius: "8px",
|
||||||
borderRadius: '4px',
|
|
||||||
color: 'white'
|
|
||||||
}}
|
}}
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '1.5rem' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.5rem',
|
|
||||||
fontSize: '1rem',
|
|
||||||
backgroundColor: '#1a1a1a',
|
|
||||||
border: '1px solid #444',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: 'white'
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.75rem',
|
|
||||||
fontSize: '1rem',
|
|
||||||
backgroundColor: loading ? '#333' : '#0066cc',
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: loading ? 'not-allowed' : 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{loading ? 'Logging in...' : 'Log In'}
|
<h1 style={{ marginBottom: "2rem", textAlign: "center" }}>
|
||||||
</button>
|
Koin Ping - Login
|
||||||
</form>
|
</h1>
|
||||||
|
|
||||||
<div style={{ marginTop: '1.5rem', textAlign: 'center' }}>
|
{error && (
|
||||||
<p style={{ color: '#999' }}>
|
<div
|
||||||
Don't have an account?{' '}
|
style={{
|
||||||
<Link
|
padding: "0.75rem",
|
||||||
to="/signup"
|
marginBottom: "1rem",
|
||||||
style={{ color: '#0066cc', textDecoration: 'none' }}
|
backgroundColor: "#ff000020",
|
||||||
>
|
border: "1px solid #ff0000",
|
||||||
Sign up here
|
borderRadius: "4px",
|
||||||
</Link>
|
color: "#ff6666",
|
||||||
</p>
|
}}
|
||||||
</div>
|
>
|
||||||
</div>
|
{error}
|
||||||
);
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div style={{ marginBottom: "1rem" }}>
|
||||||
|
<label style={{ display: "block", marginBottom: "0.5rem" }}>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "0.5rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
border: "1px solid #444",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: "1.5rem" }}>
|
||||||
|
<label style={{ display: "block", marginBottom: "0.5rem" }}>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "0.5rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
border: "1px solid #444",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "0.75rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
backgroundColor: loading ? "#333" : "#0066cc",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: loading ? "not-allowed" : "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? "Logging in..." : "Log In"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style={{ marginTop: "1.5rem", textAlign: "center" }}>
|
||||||
|
<p style={{ color: "#999" }}>
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<Link
|
||||||
|
to="/signup"
|
||||||
|
style={{ color: "#0066cc", textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
Sign up here
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,182 +4,185 @@
|
|||||||
* Allows new users to create an account with email and password
|
* Allows new users to create an account with email and password
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
export default function Signup() {
|
export default function Signup() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState("");
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { signup } = useAuth();
|
const { signup } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!email || !password || !confirmPassword) {
|
if (!email || !password || !confirmPassword) {
|
||||||
setError('Please fill in all fields');
|
setError("Please fill in all fields");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError("Passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
setError("Password must be at least 6 characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
await signup(email, password);
|
||||||
|
navigate("/addresses"); // Auto-login and redirect
|
||||||
|
} catch (err) {
|
||||||
|
// Firebase-specific error messages
|
||||||
|
if (err.code === "auth/email-already-in-use") {
|
||||||
|
setError("Email already in use. Try logging in instead.");
|
||||||
|
} else if (err.code === "auth/invalid-email") {
|
||||||
|
setError("Invalid email address");
|
||||||
|
} else if (err.code === "auth/weak-password") {
|
||||||
|
setError("Password is too weak");
|
||||||
|
} else {
|
||||||
|
setError("Failed to create account: " + err.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
return (
|
||||||
setError('Passwords do not match');
|
<div
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 6) {
|
|
||||||
setError('Password must be at least 6 characters');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setError('');
|
|
||||||
setLoading(true);
|
|
||||||
await signup(email, password);
|
|
||||||
navigate('/addresses'); // Auto-login and redirect
|
|
||||||
} catch (err) {
|
|
||||||
// Firebase-specific error messages
|
|
||||||
if (err.code === 'auth/email-already-in-use') {
|
|
||||||
setError('Email already in use. Try logging in instead.');
|
|
||||||
} else if (err.code === 'auth/invalid-email') {
|
|
||||||
setError('Invalid email address');
|
|
||||||
} else if (err.code === 'auth/weak-password') {
|
|
||||||
setError('Password is too weak');
|
|
||||||
} else {
|
|
||||||
setError('Failed to create account: ' + err.message);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
maxWidth: '400px',
|
|
||||||
margin: '4rem auto',
|
|
||||||
padding: '2rem',
|
|
||||||
border: '1px solid #333',
|
|
||||||
borderRadius: '8px'
|
|
||||||
}}>
|
|
||||||
<h1 style={{ marginBottom: '2rem', textAlign: 'center' }}>
|
|
||||||
Koin Ping - Sign Up
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div style={{
|
|
||||||
padding: '0.75rem',
|
|
||||||
marginBottom: '1rem',
|
|
||||||
backgroundColor: '#ff000020',
|
|
||||||
border: '1px solid #ff0000',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: '#ff6666'
|
|
||||||
}}>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
maxWidth: "400px",
|
||||||
padding: '0.5rem',
|
margin: "4rem auto",
|
||||||
fontSize: '1rem',
|
padding: "2rem",
|
||||||
backgroundColor: '#1a1a1a',
|
border: "1px solid #333",
|
||||||
border: '1px solid #444',
|
borderRadius: "8px",
|
||||||
borderRadius: '4px',
|
|
||||||
color: 'white'
|
|
||||||
}}
|
}}
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.5rem',
|
|
||||||
fontSize: '1rem',
|
|
||||||
backgroundColor: '#1a1a1a',
|
|
||||||
border: '1px solid #444',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: 'white'
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '1.5rem' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
|
|
||||||
Confirm Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.5rem',
|
|
||||||
fontSize: '1rem',
|
|
||||||
backgroundColor: '#1a1a1a',
|
|
||||||
border: '1px solid #444',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: 'white'
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.75rem',
|
|
||||||
fontSize: '1rem',
|
|
||||||
backgroundColor: loading ? '#333' : '#0066cc',
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: loading ? 'not-allowed' : 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{loading ? 'Creating account...' : 'Sign Up'}
|
<h1 style={{ marginBottom: "2rem", textAlign: "center" }}>
|
||||||
</button>
|
Koin Ping - Sign Up
|
||||||
</form>
|
</h1>
|
||||||
|
|
||||||
<div style={{ marginTop: '1.5rem', textAlign: 'center' }}>
|
{error && (
|
||||||
<p style={{ color: '#999' }}>
|
<div
|
||||||
Already have an account?{' '}
|
style={{
|
||||||
<Link
|
padding: "0.75rem",
|
||||||
to="/login"
|
marginBottom: "1rem",
|
||||||
style={{ color: '#0066cc', textDecoration: 'none' }}
|
backgroundColor: "#ff000020",
|
||||||
>
|
border: "1px solid #ff0000",
|
||||||
Log in here
|
borderRadius: "4px",
|
||||||
</Link>
|
color: "#ff6666",
|
||||||
</p>
|
}}
|
||||||
</div>
|
>
|
||||||
</div>
|
{error}
|
||||||
);
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div style={{ marginBottom: "1rem" }}>
|
||||||
|
<label style={{ display: "block", marginBottom: "0.5rem" }}>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "0.5rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
border: "1px solid #444",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: "1rem" }}>
|
||||||
|
<label style={{ display: "block", marginBottom: "0.5rem" }}>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "0.5rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
border: "1px solid #444",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: "1.5rem" }}>
|
||||||
|
<label style={{ display: "block", marginBottom: "0.5rem" }}>
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "0.5rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
border: "1px solid #444",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "0.75rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
backgroundColor: loading ? "#333" : "#0066cc",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: loading ? "not-allowed" : "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? "Creating account..." : "Sign Up"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style={{ marginTop: "1.5rem", textAlign: "center" }}>
|
||||||
|
<p style={{ color: "#999" }}>
|
||||||
|
Already have an account?{" "}
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
style={{ color: "#0066cc", textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
Log in here
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/v1': {
|
"/v1": {
|
||||||
target: 'http://localhost:3001',
|
target: "http://localhost:3001",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
})
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user