fmt: apply prettier to frontend JS/TS/CSS/JSON files

Initial prettier pass with tabWidth=4.
This commit is contained in:
KS Jannette
2026-02-28 20:07:28 -05:00
parent a5d1bc171c
commit da8b45012a
23 changed files with 4590 additions and 4343 deletions

View File

@@ -18,6 +18,7 @@
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"prettier": "^3.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.0" "vite": "^7.3.0"
} }
@@ -2412,6 +2413,22 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/protobufjs": { "node_modules/protobufjs": {
"version": "7.5.4", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",

View File

@@ -23,13 +23,28 @@ export default function App() {
// Show main app if authenticated // Show main app if authenticated
return ( return (
<div style={{ padding: "1rem" }}> <div style={{ padding: "1rem" }}>
<nav style={{ marginBottom: "1rem", display: "flex", justifyContent: "space-between", alignItems: "center" }}> <nav
style={{
marginBottom: "1rem",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div> <div>
<Link to="/addresses">Addresses</Link>{" | "} <Link to="/addresses">Addresses</Link>
<Link to="/alerts">Alerts</Link>{" | "} {" | "}
<Link to="/alerts">Alerts</Link>
{" | "}
<Link to="/history">History</Link> <Link to="/history">History</Link>
</div> </div>
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}> <div
style={{
display: "flex",
alignItems: "center",
gap: "1rem",
}}
>
<span style={{ fontSize: "0.9rem", color: "#999" }}> <span style={{ fontSize: "0.9rem", color: "#999" }}>
{currentUser.email} {currentUser.email}
</span> </span>
@@ -42,7 +57,7 @@ export default function App() {
color: "white", color: "white",
border: "1px solid #555", border: "1px solid #555",
borderRadius: "4px", borderRadius: "4px",
cursor: "pointer" cursor: "pointer",
}} }}
> >
Logout Logout

View File

@@ -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
@@ -14,14 +14,14 @@ 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;
@@ -35,8 +35,10 @@ export async function createAddress(data) {
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;
} }
@@ -50,12 +52,12 @@ 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;
@@ -69,8 +71,10 @@ export async function getAddresses() {
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;
} }
@@ -85,13 +89,13 @@ 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;
@@ -103,10 +107,11 @@ export async function deleteAddress(addressId) {
} }
} 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;
} }
} }

View File

@@ -1,7 +1,7 @@
// 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)
@@ -11,11 +11,11 @@ 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;
@@ -27,8 +27,10 @@ export async function getAlertEvents() {
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;
} }

View File

@@ -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",
}; };
/** /**
@@ -24,14 +24,17 @@ export const ALERT_TYPES = {
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`,
{
method: "POST",
headers: headers, headers: headers,
body: JSON.stringify(data) 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;
@@ -43,8 +46,10 @@ export async function createAlert(addressId, data) {
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;
} }
@@ -58,12 +63,15 @@ export async function createAlert(addressId, data) {
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;
@@ -75,8 +83,10 @@ export async function getAlerts(addressId) {
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;
} }
@@ -92,13 +102,13 @@ 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;
@@ -110,8 +120,10 @@ export async function updateAlertStatus(alertId, enabled) {
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;
} }
@@ -126,12 +138,12 @@ 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;
@@ -141,8 +153,10 @@ export async function deleteAlert(alertId) {
throw new Error(errorMessage); throw new Error(errorMessage);
} }
} 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;
} }

View File

@@ -5,7 +5,7 @@
* 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
@@ -15,15 +15,15 @@ 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}`,
}; };
} }
@@ -35,13 +35,12 @@ 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}`,
}; };
} }

View File

@@ -1 +1 @@
export const API_BASE = import.meta.env.VITE_API_BASE || '/v1'; export const API_BASE = import.meta.env.VITE_API_BASE || "/v1";

View File

@@ -1,7 +1,7 @@
// 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
@@ -11,11 +11,11 @@ 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;
@@ -27,8 +27,10 @@ export async function getNotificationConfig() {
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;
} }
@@ -47,13 +49,13 @@ 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;
@@ -65,8 +67,10 @@ export async function updateNotificationConfig(config) {
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;
} }
@@ -81,19 +85,19 @@ export async function updateNotificationConfig(config) {
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;
} }
} }

View File

@@ -1,6 +1,6 @@
// 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
@@ -11,7 +11,7 @@ export async function getSystemStatus() {
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();

View File

@@ -45,8 +45,8 @@ export default function AlertForm({ onSubmit }) {
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>

View File

@@ -20,4 +20,3 @@ export default function Button({
</button> </button>
); );
} }

View File

@@ -30,4 +30,3 @@ export default function Input({
</label> </label>
); );
} }

View File

@@ -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();
@@ -22,7 +22,7 @@ const AuthContext = createContext();
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;
} }
@@ -41,7 +41,11 @@ export function AuthProvider({ children }) {
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(
auth,
email,
password,
);
return result.user; return result.user;
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
@@ -55,7 +59,11 @@ export function AuthProvider({ children }) {
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(
auth,
email,
password,
);
return result.user; return result.user;
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
@@ -95,7 +103,7 @@ export function AuthProvider({ children }) {
login, login,
logout, logout,
error, error,
loading loading,
}; };
return ( return (
@@ -104,4 +112,3 @@ export function AuthProvider({ children }) {
</AuthContext.Provider> </AuthContext.Provider>
); );
} }

View File

@@ -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,7 +17,7 @@ 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);
@@ -27,4 +25,3 @@ const app = initializeApp(firebaseConfig);
export const auth = getAuth(app); export const auth = getAuth(app);
export default app; export default app;

View File

@@ -11,5 +11,5 @@ ReactDOM.createRoot(document.getElementById("root")).render(
<App /> <App />
</AuthProvider> </AuthProvider>
</BrowserRouter> </BrowserRouter>
</React.StrictMode> </React.StrictMode>,
); );

View File

@@ -67,10 +67,20 @@ export default function Addresses() {
borderRadius: "4px", borderRadius: "4px",
}} }}
> >
<div style={{ fontWeight: "bold", marginBottom: "0.25rem" }}> <div
style={{
fontWeight: "bold",
marginBottom: "0.25rem",
}}
>
{addr.label || "Unlabeled"} {addr.label || "Unlabeled"}
</div> </div>
<div style={{ fontFamily: "monospace", fontSize: "0.9rem" }}> <div
style={{
fontFamily: "monospace",
fontSize: "0.9rem",
}}
>
{addr.address} {addr.address}
</div> </div>
</li> </li>

View File

@@ -29,7 +29,9 @@ export default function AlertHistory() {
} }
if (error) { if (error) {
return <div style={{ padding: "2rem", color: "red" }}>Error: {error}</div>; return (
<div style={{ padding: "2rem", color: "red" }}>Error: {error}</div>
);
} }
return ( return (
@@ -50,9 +52,17 @@ export default function AlertHistory() {
borderRadius: "4px", borderRadius: "4px",
}} }}
> >
<div style={{ marginBottom: "0.5rem" }}>{event.message}</div> <div style={{ marginBottom: "0.5rem" }}>
{event.message}
</div>
{event.address_label && ( {event.address_label && (
<div style={{ fontSize: "0.9rem", color: "#666", marginBottom: "0.25rem" }}> <div
style={{
fontSize: "0.9rem",
color: "#666",
marginBottom: "0.25rem",
}}
>
Address: {event.address_label} Address: {event.address_label}
</div> </div>
)} )}

View File

@@ -3,8 +3,17 @@ import AlertForm from "../components/AlertForm";
import Button from "../components/Button"; import Button from "../components/Button";
import Input from "../components/Input"; import Input from "../components/Input";
import { getAddresses } from "../api/addresses"; import { getAddresses } from "../api/addresses";
import { getAlerts, createAlert, updateAlertStatus, deleteAlert } from "../api/alerts"; import {
import { getNotificationConfig, updateNotificationConfig, testDiscordWebhook } from "../api/notificationConfig"; getAlerts,
createAlert,
updateAlertStatus,
deleteAlert,
} from "../api/alerts";
import {
getNotificationConfig,
updateNotificationConfig,
testDiscordWebhook,
} from "../api/notificationConfig";
export default function Alerts() { export default function Alerts() {
const [addresses, setAddresses] = useState([]); const [addresses, setAddresses] = useState([]);
@@ -15,7 +24,7 @@ export default function Alerts() {
// Notification config state // Notification config state
const [notificationConfig, setNotificationConfig] = useState(null); const [notificationConfig, setNotificationConfig] = useState(null);
const [discordWebhookUrl, setDiscordWebhookUrl] = useState(''); const [discordWebhookUrl, setDiscordWebhookUrl] = useState("");
const [notificationEnabled, setNotificationEnabled] = useState(true); const [notificationEnabled, setNotificationEnabled] = useState(true);
const [notificationLoading, setNotificationLoading] = useState(false); const [notificationLoading, setNotificationLoading] = useState(false);
const [notificationError, setNotificationError] = useState(null); const [notificationError, setNotificationError] = useState(null);
@@ -38,8 +47,10 @@ export default function Alerts() {
// Fetch notification config // Fetch notification config
const configData = await getNotificationConfig(); const configData = await getNotificationConfig();
setNotificationConfig(configData); setNotificationConfig(configData);
setDiscordWebhookUrl(configData.discord_webhook_url || ''); setDiscordWebhookUrl(configData.discord_webhook_url || "");
setNotificationEnabled(configData.notification_enabled !== false); setNotificationEnabled(
configData.notification_enabled !== false,
);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
console.error("Failed to fetch data:", err); console.error("Failed to fetch data:", err);
@@ -91,7 +102,7 @@ export default function Alerts() {
try { try {
const updated = await updateAlertStatus(alertId, !currentStatus); const updated = await updateAlertStatus(alertId, !currentStatus);
setAlerts((prev) => setAlerts((prev) =>
prev.map((alert) => (alert.id === alertId ? updated : alert)) prev.map((alert) => (alert.id === alertId ? updated : alert)),
); );
setError(null); // Clear any previous errors setError(null); // Clear any previous errors
} catch (err) { } catch (err) {
@@ -126,7 +137,7 @@ export default function Alerts() {
const updated = await updateNotificationConfig(config); const updated = await updateNotificationConfig(config);
setNotificationConfig(updated); setNotificationConfig(updated);
setNotificationSuccess('Notification settings saved!'); setNotificationSuccess("Notification settings saved!");
// Clear success message after 3 seconds // Clear success message after 3 seconds
setTimeout(() => setNotificationSuccess(null), 3000); setTimeout(() => setNotificationSuccess(null), 3000);
@@ -141,7 +152,7 @@ export default function Alerts() {
// Test Discord webhook // Test Discord webhook
async function handleTestWebhook() { async function handleTestWebhook() {
if (!discordWebhookUrl) { if (!discordWebhookUrl) {
setNotificationError('Please enter a Discord webhook URL first'); setNotificationError("Please enter a Discord webhook URL first");
return; return;
} }
@@ -153,13 +164,15 @@ export default function Alerts() {
const success = await testDiscordWebhook(discordWebhookUrl); const success = await testDiscordWebhook(discordWebhookUrl);
if (success) { if (success) {
setNotificationSuccess('Test notification sent! Check your Discord channel.'); setNotificationSuccess(
"Test notification sent! Check your Discord channel.",
);
setTimeout(() => setNotificationSuccess(null), 5000); setTimeout(() => setNotificationSuccess(null), 5000);
} else { } else {
setNotificationError('Test failed. Check your webhook URL.'); setNotificationError("Test failed. Check your webhook URL.");
} }
} catch (err) { } catch (err) {
setNotificationError('Test failed: ' + err.message); setNotificationError("Test failed: " + err.message);
} finally { } finally {
setTestingWebhook(false); setTestingWebhook(false);
} }
@@ -174,30 +187,44 @@ export default function Alerts() {
if (addresses.length === 0) { if (addresses.length === 0) {
return ( return (
<div style={{ padding: "2rem" }}> <div style={{ padding: "2rem" }}>
<p>No addresses tracked yet. Add an address first to create alerts.</p> <p>
No addresses tracked yet. Add an address first to create
alerts.
</p>
</div> </div>
); );
} }
return ( return (
<div style={{ maxWidth: "1400px", margin: "0 auto", padding: "2rem" }}> <div style={{ maxWidth: "1400px", margin: "0 auto", padding: "2rem" }}>
<h1 style={{ marginBottom: "2rem" }}>Alert Rules & Notifications</h1> <h1 style={{ marginBottom: "2rem" }}>
Alert Rules & Notifications
</h1>
{/* Two-column layout */} {/* Two-column layout */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "2rem" }}> <div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "2rem",
}}
>
{/* LEFT COLUMN: Alert Rules */} {/* LEFT COLUMN: Alert Rules */}
<div> <div>
<h2 style={{ marginTop: 0 }}>Alert Rules</h2> <h2 style={{ marginTop: 0 }}>Alert Rules</h2>
{/* Address selector */} {/* Address selector */}
<div style={{ marginBottom: "2rem" }}> <div style={{ marginBottom: "2rem" }}>
<label style={{ display: "block", marginBottom: "0.5rem" }}> <label
style={{ display: "block", marginBottom: "0.5rem" }}
>
<strong>Select Address:</strong> <strong>Select Address:</strong>
</label> </label>
<select <select
value={selectedAddressId || ""} value={selectedAddressId || ""}
onChange={(e) => setSelectedAddressId(Number(e.target.value))} onChange={(e) =>
setSelectedAddressId(Number(e.target.value))
}
style={{ style={{
width: "100%", width: "100%",
padding: "0.5rem", padding: "0.5rem",
@@ -205,7 +232,7 @@ export default function Alerts() {
backgroundColor: "#1a1a1a", backgroundColor: "#1a1a1a",
border: "1px solid #444", border: "1px solid #444",
borderRadius: "4px", borderRadius: "4px",
color: "white" color: "white",
}} }}
> >
{addresses.map((addr) => ( {addresses.map((addr) => (
@@ -225,16 +252,32 @@ export default function Alerts() {
marginBottom: "2rem", marginBottom: "2rem",
backgroundColor: "#2a2a2a", backgroundColor: "#2a2a2a",
borderRadius: "4px", borderRadius: "4px",
border: "1px solid #444" border: "1px solid #444",
}}
>
<div
style={{
fontSize: "0.9rem",
color: "#999",
}} }}
> >
<div style={{ fontSize: "0.9rem", color: "#999" }}>
Managing alerts for: Managing alerts for:
</div> </div>
<div style={{ fontWeight: "bold", marginTop: "0.25rem" }}> <div
style={{
fontWeight: "bold",
marginTop: "0.25rem",
}}
>
{selectedAddress.label || "Unlabeled"} {selectedAddress.label || "Unlabeled"}
</div> </div>
<div style={{ fontFamily: "monospace", fontSize: "0.9rem", color: "#999" }}> <div
style={{
fontFamily: "monospace",
fontSize: "0.9rem",
color: "#999",
}}
>
{selectedAddress.address} {selectedAddress.address}
</div> </div>
</div> </div>
@@ -248,13 +291,23 @@ export default function Alerts() {
{/* Existing alerts list */} {/* Existing alerts list */}
<div> <div>
<h3>Active Alert Rules</h3> <h3>Active Alert Rules</h3>
{error && <p style={{ color: "red" }}>Error: {error}</p>} {error && (
<p style={{ color: "red" }}>
Error: {error}
</p>
)}
{alerts.length === 0 ? ( {alerts.length === 0 ? (
<p style={{ color: "#666" }}> <p style={{ color: "#666" }}>
No alert rules defined yet. Create one above. No alert rules defined yet. Create one
above.
</p> </p>
) : ( ) : (
<ul style={{ listStyle: "none", padding: 0 }}> <ul
style={{
listStyle: "none",
padding: 0,
}}
>
{alerts.map((alert) => ( {alerts.map((alert) => (
<li <li
key={alert.id} key={alert.id}
@@ -264,36 +317,88 @@ export default function Alerts() {
border: "1px solid #444", border: "1px solid #444",
borderRadius: "4px", borderRadius: "4px",
backgroundColor: "#2a2a2a", backgroundColor: "#2a2a2a",
opacity: alert.enabled ? 1 : 0.6, opacity: alert.enabled
? 1
: 0.6,
}} }}
> >
<div <div
style={{ style={{
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent:
alignItems: "flex-start", "space-between",
alignItems:
"flex-start",
}} }}
> >
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ fontWeight: "bold", marginBottom: "0.25rem" }}> <div
{formatAlertType(alert.type)} style={{
fontWeight:
"bold",
marginBottom:
"0.25rem",
}}
>
{formatAlertType(
alert.type,
)}
</div> </div>
{alert.threshold && ( {alert.threshold && (
<div style={{ fontSize: "0.9rem", color: "#999" }}> <div
Threshold: {alert.threshold} ETH style={{
fontSize:
"0.9rem",
color: "#999",
}}
>
Threshold:{" "}
{
alert.threshold
}{" "}
ETH
</div> </div>
)} )}
<div style={{ fontSize: "0.85rem", color: "#666", marginTop: "0.25rem" }}> <div
Status: {alert.enabled ? "Enabled" : "Disabled"} style={{
</div> fontSize:
</div> "0.85rem",
<div style={{ display: "flex", gap: "0.5rem" }}> color: "#666",
<Button marginTop:
onClick={() => handleToggleAlert(alert.id, alert.enabled)} "0.25rem",
}}
> >
{alert.enabled ? "Disable" : "Enable"} Status:{" "}
{alert.enabled
? "Enabled"
: "Disabled"}
</div>
</div>
<div
style={{
display: "flex",
gap: "0.5rem",
}}
>
<Button
onClick={() =>
handleToggleAlert(
alert.id,
alert.enabled,
)
}
>
{alert.enabled
? "Disable"
: "Enable"}
</Button> </Button>
<Button onClick={() => handleDeleteAlert(alert.id)}> <Button
onClick={() =>
handleDeleteAlert(
alert.id,
)
}
>
Delete Delete
</Button> </Button>
</div> </div>
@@ -312,148 +417,208 @@ export default function Alerts() {
<h2 style={{ marginTop: 0 }}>Notification Settings</h2> <h2 style={{ marginTop: 0 }}>Notification Settings</h2>
{notificationSuccess && ( {notificationSuccess && (
<div style={{ <div
padding: '0.75rem', style={{
marginBottom: '1rem', padding: "0.75rem",
backgroundColor: '#00ff0020', marginBottom: "1rem",
border: '1px solid #00ff00', backgroundColor: "#00ff0020",
borderRadius: '4px', border: "1px solid #00ff00",
color: '#00ff00' borderRadius: "4px",
}}> color: "#00ff00",
}}
>
{notificationSuccess} {notificationSuccess}
</div> </div>
)} )}
{notificationError && ( {notificationError && (
<div style={{ <div
padding: '0.75rem', style={{
marginBottom: '1rem', padding: "0.75rem",
backgroundColor: '#ff000020', marginBottom: "1rem",
border: '1px solid #ff0000', backgroundColor: "#ff000020",
borderRadius: '4px', border: "1px solid #ff0000",
color: '#ff6666' borderRadius: "4px",
}}> color: "#ff6666",
}}
>
{notificationError} {notificationError}
</div> </div>
)} )}
{/* Master toggle */} {/* Master toggle */}
<div style={{ marginBottom: '2rem', padding: '1rem', backgroundColor: '#f5f5f5', borderRadius: '4px' }}> <div
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}> style={{
marginBottom: "2rem",
padding: "1rem",
backgroundColor: "#f5f5f5",
borderRadius: "4px",
}}
>
<label
style={{
display: "flex",
alignItems: "center",
cursor: "pointer",
}}
>
<input <input
type="checkbox" type="checkbox"
checked={notificationEnabled} checked={notificationEnabled}
onChange={(e) => setNotificationEnabled(e.target.checked)} onChange={(e) =>
style={{ marginRight: '0.5rem', width: '18px', height: '18px' }} setNotificationEnabled(e.target.checked)
}
style={{
marginRight: "0.5rem",
width: "18px",
height: "18px",
}}
/> />
<span style={{ fontWeight: 'bold' }}>Enable Notifications</span> <span style={{ fontWeight: "bold" }}>
Enable Notifications
</span>
</label> </label>
<div style={{ fontSize: '0.85rem', color: '#666', marginTop: '0.5rem', marginLeft: '26px' }}> <div
style={{
fontSize: "0.85rem",
color: "#666",
marginTop: "0.5rem",
marginLeft: "26px",
}}
>
Master switch for all notification channels Master switch for all notification channels
</div> </div>
</div> </div>
{/* Discord Section */} {/* Discord Section */}
<div style={{ marginBottom: '2rem' }}> <div style={{ marginBottom: "2rem" }}>
<h3 style={{ marginBottom: '1rem' }}>Discord</h3> <h3 style={{ marginBottom: "1rem" }}>Discord</h3>
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: "1rem" }}>
<label style={{ display: 'block', marginBottom: '0.5rem' }}> <label
style={{
display: "block",
marginBottom: "0.5rem",
}}
>
Discord Webhook URL Discord Webhook URL
</label> </label>
<input <input
type="text" type="text"
value={discordWebhookUrl} value={discordWebhookUrl}
onChange={(e) => setDiscordWebhookUrl(e.target.value)} onChange={(e) =>
setDiscordWebhookUrl(e.target.value)
}
placeholder="https://discord.com/api/webhooks/..." placeholder="https://discord.com/api/webhooks/..."
style={{ style={{
width: '100%', width: "100%",
padding: '0.5rem', padding: "0.5rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: '#1a1a1a', backgroundColor: "#1a1a1a",
border: '1px solid #444', border: "1px solid #444",
borderRadius: '4px', borderRadius: "4px",
color: 'white', color: "white",
fontFamily: 'monospace', fontFamily: "monospace",
fontSize: '0.9rem' fontSize: "0.9rem",
}} }}
/> />
<div style={{ fontSize: '0.85rem', color: '#999', marginTop: '0.5rem' }}> <div
style={{
fontSize: "0.85rem",
color: "#999",
marginTop: "0.5rem",
}}
>
<a <a
href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ color: '#0066cc' }} style={{ color: "#0066cc" }}
> >
How to get a Discord webhook URL How to get a Discord webhook URL
</a> </a>
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: '0.5rem' }}> <div style={{ display: "flex", gap: "0.5rem" }}>
<button <button
onClick={handleSaveNotificationConfig} onClick={handleSaveNotificationConfig}
disabled={notificationLoading} disabled={notificationLoading}
style={{ style={{
padding: '0.75rem 1.5rem', padding: "0.75rem 1.5rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: notificationLoading ? '#333' : '#0066cc', backgroundColor: notificationLoading
color: 'white', ? "#333"
border: 'none', : "#0066cc",
borderRadius: '4px', color: "white",
cursor: notificationLoading ? 'not-allowed' : 'pointer' border: "none",
borderRadius: "4px",
cursor: notificationLoading
? "not-allowed"
: "pointer",
}} }}
> >
{notificationLoading ? 'Saving...' : 'Save Settings'} {notificationLoading
? "Saving..."
: "Save Settings"}
</button> </button>
<button <button
onClick={handleTestWebhook} onClick={handleTestWebhook}
disabled={testingWebhook || !discordWebhookUrl} disabled={testingWebhook || !discordWebhookUrl}
style={{ style={{
padding: '0.75rem 1.5rem', padding: "0.75rem 1.5rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: testingWebhook || !discordWebhookUrl ? '#333' : '#28a745', backgroundColor:
color: 'white', testingWebhook || !discordWebhookUrl
border: 'none', ? "#333"
borderRadius: '4px', : "#28a745",
cursor: testingWebhook || !discordWebhookUrl ? 'not-allowed' : 'pointer' color: "white",
border: "none",
borderRadius: "4px",
cursor:
testingWebhook || !discordWebhookUrl
? "not-allowed"
: "pointer",
}} }}
> >
{testingWebhook ? 'Testing...' : 'Test Webhook'} {testingWebhook ? "Testing..." : "Test Webhook"}
</button> </button>
</div> </div>
</div> </div>
{/* Telegram Section (Coming Soon) */} {/* Telegram Section (Coming Soon) */}
<div style={{ marginBottom: '2rem', opacity: 0.5 }}> <div style={{ marginBottom: "2rem", opacity: 0.5 }}>
<h3 style={{ marginBottom: '1rem' }}>Telegram</h3> <h3 style={{ marginBottom: "1rem" }}>Telegram</h3>
<div style={{ <div
padding: '1rem', style={{
backgroundColor: '#333', padding: "1rem",
borderRadius: '4px', backgroundColor: "#333",
color: '#999', borderRadius: "4px",
textAlign: 'center' color: "#999",
}}> textAlign: "center",
}}
>
Coming Soon Coming Soon
</div> </div>
</div> </div>
{/* Email Section (Coming Soon) */} {/* Email Section (Coming Soon) */}
<div style={{ opacity: 0.5 }}> <div style={{ opacity: 0.5 }}>
<h3 style={{ marginBottom: '1rem' }}>Email</h3> <h3 style={{ marginBottom: "1rem" }}>Email</h3>
<div style={{ <div
padding: '1rem', style={{
backgroundColor: '#333', padding: "1rem",
borderRadius: '4px', backgroundColor: "#333",
color: '#999', borderRadius: "4px",
textAlign: 'center' color: "#999",
}}> textAlign: "center",
}}
>
Coming Soon Coming Soon
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );

View File

@@ -4,14 +4,14 @@
* 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();
@@ -22,50 +22,54 @@ export default function Login() {
// Validation // Validation
if (!email || !password) { if (!email || !password) {
setError('Please fill in all fields'); setError("Please fill in all fields");
return; return;
} }
try { try {
setError(''); setError("");
setLoading(true); setLoading(true);
await login(email, password); await login(email, password);
navigate('/addresses'); // Redirect to main app navigate("/addresses"); // Redirect to main app
} catch (err) { } catch (err) {
setError('Failed to log in: ' + err.message); setError("Failed to log in: " + err.message);
} finally { } finally {
setLoading(false); setLoading(false);
} }
} }
return ( return (
<div style={{ <div
maxWidth: '400px', style={{
margin: '4rem auto', maxWidth: "400px",
padding: '2rem', margin: "4rem auto",
border: '1px solid #333', padding: "2rem",
borderRadius: '8px' border: "1px solid #333",
}}> borderRadius: "8px",
<h1 style={{ marginBottom: '2rem', textAlign: 'center' }}> }}
>
<h1 style={{ marginBottom: "2rem", textAlign: "center" }}>
Koin Ping - Login Koin Ping - Login
</h1> </h1>
{error && ( {error && (
<div style={{ <div
padding: '0.75rem', style={{
marginBottom: '1rem', padding: "0.75rem",
backgroundColor: '#ff000020', marginBottom: "1rem",
border: '1px solid #ff0000', backgroundColor: "#ff000020",
borderRadius: '4px', border: "1px solid #ff0000",
color: '#ff6666' borderRadius: "4px",
}}> color: "#ff6666",
}}
>
{error} {error}
</div> </div>
)} )}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: "1rem" }}>
<label style={{ display: 'block', marginBottom: '0.5rem' }}> <label style={{ display: "block", marginBottom: "0.5rem" }}>
Email Email
</label> </label>
<input <input
@@ -74,20 +78,20 @@ export default function Login() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
disabled={loading} disabled={loading}
style={{ style={{
width: '100%', width: "100%",
padding: '0.5rem', padding: "0.5rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: '#1a1a1a', backgroundColor: "#1a1a1a",
border: '1px solid #444', border: "1px solid #444",
borderRadius: '4px', borderRadius: "4px",
color: 'white' color: "white",
}} }}
required required
/> />
</div> </div>
<div style={{ marginBottom: '1.5rem' }}> <div style={{ marginBottom: "1.5rem" }}>
<label style={{ display: 'block', marginBottom: '0.5rem' }}> <label style={{ display: "block", marginBottom: "0.5rem" }}>
Password Password
</label> </label>
<input <input
@@ -96,13 +100,13 @@ export default function Login() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
disabled={loading} disabled={loading}
style={{ style={{
width: '100%', width: "100%",
padding: '0.5rem', padding: "0.5rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: '#1a1a1a', backgroundColor: "#1a1a1a",
border: '1px solid #444', border: "1px solid #444",
borderRadius: '4px', borderRadius: "4px",
color: 'white' color: "white",
}} }}
required required
/> />
@@ -112,26 +116,26 @@ export default function Login() {
type="submit" type="submit"
disabled={loading} disabled={loading}
style={{ style={{
width: '100%', width: "100%",
padding: '0.75rem', padding: "0.75rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: loading ? '#333' : '#0066cc', backgroundColor: loading ? "#333" : "#0066cc",
color: 'white', color: "white",
border: 'none', border: "none",
borderRadius: '4px', borderRadius: "4px",
cursor: loading ? 'not-allowed' : 'pointer' cursor: loading ? "not-allowed" : "pointer",
}} }}
> >
{loading ? 'Logging in...' : 'Log In'} {loading ? "Logging in..." : "Log In"}
</button> </button>
</form> </form>
<div style={{ marginTop: '1.5rem', textAlign: 'center' }}> <div style={{ marginTop: "1.5rem", textAlign: "center" }}>
<p style={{ color: '#999' }}> <p style={{ color: "#999" }}>
Don't have an account?{' '} Don't have an account?{" "}
<Link <Link
to="/signup" to="/signup"
style={{ color: '#0066cc', textDecoration: 'none' }} style={{ color: "#0066cc", textDecoration: "none" }}
> >
Sign up here Sign up here
</Link> </Link>
@@ -140,4 +144,3 @@ export default function Login() {
</div> </div>
); );
} }

View File

@@ -4,15 +4,15 @@
* 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();
@@ -23,35 +23,35 @@ export default function Signup() {
// 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) { if (password !== confirmPassword) {
setError('Passwords do not match'); setError("Passwords do not match");
return; return;
} }
if (password.length < 6) { if (password.length < 6) {
setError('Password must be at least 6 characters'); setError("Password must be at least 6 characters");
return; return;
} }
try { try {
setError(''); setError("");
setLoading(true); setLoading(true);
await signup(email, password); await signup(email, password);
navigate('/addresses'); // Auto-login and redirect navigate("/addresses"); // Auto-login and redirect
} catch (err) { } catch (err) {
// Firebase-specific error messages // Firebase-specific error messages
if (err.code === 'auth/email-already-in-use') { if (err.code === "auth/email-already-in-use") {
setError('Email already in use. Try logging in instead.'); setError("Email already in use. Try logging in instead.");
} else if (err.code === 'auth/invalid-email') { } else if (err.code === "auth/invalid-email") {
setError('Invalid email address'); setError("Invalid email address");
} else if (err.code === 'auth/weak-password') { } else if (err.code === "auth/weak-password") {
setError('Password is too weak'); setError("Password is too weak");
} else { } else {
setError('Failed to create account: ' + err.message); setError("Failed to create account: " + err.message);
} }
} finally { } finally {
setLoading(false); setLoading(false);
@@ -59,33 +59,37 @@ export default function Signup() {
} }
return ( return (
<div style={{ <div
maxWidth: '400px', style={{
margin: '4rem auto', maxWidth: "400px",
padding: '2rem', margin: "4rem auto",
border: '1px solid #333', padding: "2rem",
borderRadius: '8px' border: "1px solid #333",
}}> borderRadius: "8px",
<h1 style={{ marginBottom: '2rem', textAlign: 'center' }}> }}
>
<h1 style={{ marginBottom: "2rem", textAlign: "center" }}>
Koin Ping - Sign Up Koin Ping - Sign Up
</h1> </h1>
{error && ( {error && (
<div style={{ <div
padding: '0.75rem', style={{
marginBottom: '1rem', padding: "0.75rem",
backgroundColor: '#ff000020', marginBottom: "1rem",
border: '1px solid #ff0000', backgroundColor: "#ff000020",
borderRadius: '4px', border: "1px solid #ff0000",
color: '#ff6666' borderRadius: "4px",
}}> color: "#ff6666",
}}
>
{error} {error}
</div> </div>
)} )}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: "1rem" }}>
<label style={{ display: 'block', marginBottom: '0.5rem' }}> <label style={{ display: "block", marginBottom: "0.5rem" }}>
Email Email
</label> </label>
<input <input
@@ -94,20 +98,20 @@ export default function Signup() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
disabled={loading} disabled={loading}
style={{ style={{
width: '100%', width: "100%",
padding: '0.5rem', padding: "0.5rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: '#1a1a1a', backgroundColor: "#1a1a1a",
border: '1px solid #444', border: "1px solid #444",
borderRadius: '4px', borderRadius: "4px",
color: 'white' color: "white",
}} }}
required required
/> />
</div> </div>
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: "1rem" }}>
<label style={{ display: 'block', marginBottom: '0.5rem' }}> <label style={{ display: "block", marginBottom: "0.5rem" }}>
Password Password
</label> </label>
<input <input
@@ -116,20 +120,20 @@ export default function Signup() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
disabled={loading} disabled={loading}
style={{ style={{
width: '100%', width: "100%",
padding: '0.5rem', padding: "0.5rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: '#1a1a1a', backgroundColor: "#1a1a1a",
border: '1px solid #444', border: "1px solid #444",
borderRadius: '4px', borderRadius: "4px",
color: 'white' color: "white",
}} }}
required required
/> />
</div> </div>
<div style={{ marginBottom: '1.5rem' }}> <div style={{ marginBottom: "1.5rem" }}>
<label style={{ display: 'block', marginBottom: '0.5rem' }}> <label style={{ display: "block", marginBottom: "0.5rem" }}>
Confirm Password Confirm Password
</label> </label>
<input <input
@@ -138,13 +142,13 @@ export default function Signup() {
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
disabled={loading} disabled={loading}
style={{ style={{
width: '100%', width: "100%",
padding: '0.5rem', padding: "0.5rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: '#1a1a1a', backgroundColor: "#1a1a1a",
border: '1px solid #444', border: "1px solid #444",
borderRadius: '4px', borderRadius: "4px",
color: 'white' color: "white",
}} }}
required required
/> />
@@ -154,26 +158,26 @@ export default function Signup() {
type="submit" type="submit"
disabled={loading} disabled={loading}
style={{ style={{
width: '100%', width: "100%",
padding: '0.75rem', padding: "0.75rem",
fontSize: '1rem', fontSize: "1rem",
backgroundColor: loading ? '#333' : '#0066cc', backgroundColor: loading ? "#333" : "#0066cc",
color: 'white', color: "white",
border: 'none', border: "none",
borderRadius: '4px', borderRadius: "4px",
cursor: loading ? 'not-allowed' : 'pointer' cursor: loading ? "not-allowed" : "pointer",
}} }}
> >
{loading ? 'Creating account...' : 'Sign Up'} {loading ? "Creating account..." : "Sign Up"}
</button> </button>
</form> </form>
<div style={{ marginTop: '1.5rem', textAlign: 'center' }}> <div style={{ marginTop: "1.5rem", textAlign: "center" }}>
<p style={{ color: '#999' }}> <p style={{ color: "#999" }}>
Already have an account?{' '} Already have an account?{" "}
<Link <Link
to="/login" to="/login"
style={{ color: '#0066cc', textDecoration: 'none' }} style={{ color: "#0066cc", textDecoration: "none" }}
> >
Log in here Log in here
</Link> </Link>
@@ -182,4 +186,3 @@ export default function Signup() {
</div> </div>
); );
} }

View File

@@ -22,4 +22,3 @@
}, },
"include": ["src"] "include": ["src"]
} }

View File

@@ -1,5 +1,5 @@
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({
@@ -7,11 +7,10 @@ export default defineConfig({
server: { server: {
port: 3000, port: 3000,
proxy: { proxy: {
'/v1': { "/v1": {
target: 'http://localhost:3001', target: "http://localhost:3001",
changeOrigin: true, changeOrigin: true,
}, },
}, },
}, },
}) });