refact css sytem
This commit is contained in:
@@ -37,7 +37,7 @@ export default function AddressForm({ onSubmit }) {
|
||||
placeholder="Treasury, Cold Wallet, etc."
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={!canSubmit}>
|
||||
<Button type="submit" disabled={!canSubmit} className="mt-md">
|
||||
Add Address
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
7
frontend/src/components/AlertForm.css
Normal file
7
frontend/src/components/AlertForm.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.alert-form__options {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-form__radio {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import Input from "./Input";
|
||||
import Button from "./Button";
|
||||
import "./AlertForm.css";
|
||||
|
||||
const ALERT_TYPES = [
|
||||
{ value: "incoming_tx", label: "Incoming transaction" },
|
||||
@@ -36,9 +37,9 @@ export default function AlertForm({ onSubmit }) {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: "1rem" }}>
|
||||
<div className="alert-form__options">
|
||||
{ALERT_TYPES.map((opt) => (
|
||||
<label key={opt.value} style={{ display: "block" }}>
|
||||
<label key={opt.value} className="alert-form__radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="alertType"
|
||||
|
||||
14
frontend/src/components/Button.css
Normal file
14
frontend/src/components/Button.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
color: var(--color-text);
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
import "./Button.css";
|
||||
|
||||
export default function Button({
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
type = "button",
|
||||
variant = "primary",
|
||||
className = "",
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
fontSize: "1rem",
|
||||
cursor: disabled ? "not-allowed" : "pointer",
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
}}
|
||||
className={`btn btn--${variant} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
21
frontend/src/components/Input.css
Normal file
21
frontend/src/components/Input.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.input__label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.input__control {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
background-color: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.input__control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.25);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./Input.css";
|
||||
|
||||
export default function Input({
|
||||
label,
|
||||
type = "text",
|
||||
@@ -7,12 +9,12 @@ export default function Input({
|
||||
step,
|
||||
min,
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = "",
|
||||
}) {
|
||||
return (
|
||||
<label style={{ display: "block", marginBottom: "1rem" }}>
|
||||
<div style={{ marginBottom: "0.25rem", fontSize: "0.9rem" }}>
|
||||
{label}
|
||||
</div>
|
||||
<label className={`form-field ${className}`}>
|
||||
<div className="input__label">{label}</div>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
@@ -20,12 +22,9 @@ export default function Input({
|
||||
step={step}
|
||||
min={min}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
className="input__control"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
|
||||
56
frontend/src/components/Navbar.css
Normal file
56
frontend/src/components/Navbar.css
Normal file
@@ -0,0 +1,56 @@
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background-color: var(--color-bg);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.navbar__brand {
|
||||
font-weight: 700;
|
||||
font-size: 1.4rem;
|
||||
color: var(--color-brand);
|
||||
margin-right: 2rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.navbar__links {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.navbar__link {
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: var(--radius-lg);
|
||||
text-decoration: none;
|
||||
font-size: 1.425rem;
|
||||
font-weight: 200;
|
||||
color: var(--color-text-subtle);
|
||||
background-color: transparent;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.navbar__link--active {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.navbar__user {
|
||||
font-size: 1.275rem;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.navbar__logout {
|
||||
padding: 0.4rem 1rem;
|
||||
font-size: 1.275rem;
|
||||
font-weight: 400;
|
||||
background-color: transparent;
|
||||
color: var(--color-text-subtle);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import "./Navbar.css";
|
||||
|
||||
const navLinks = [
|
||||
{ to: "/addresses", label: "Addresses" },
|
||||
@@ -14,43 +15,17 @@ export default function Navbar() {
|
||||
if (!currentUser) return null;
|
||||
|
||||
return (
|
||||
<nav style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "1rem 2rem",
|
||||
marginBottom: "1.5rem",
|
||||
backgroundColor: "#1a1a1a",
|
||||
borderBottom: "1px solid #333",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||
<span style={{
|
||||
fontWeight: 700,
|
||||
fontSize: "1.4rem",
|
||||
color: "#e62525",
|
||||
marginRight: "2rem",
|
||||
letterSpacing: "0.5px",
|
||||
}}>
|
||||
Koin Ping
|
||||
</span>
|
||||
|
||||
<div style={{ display: "flex", gap: "0.25rem" }}>
|
||||
<nav className="navbar">
|
||||
<div className="flex flex--center gap-sm">
|
||||
<span className="navbar__brand">Koin Ping</span>
|
||||
<div className="navbar__links">
|
||||
{navLinks.map(({ to, label }) => {
|
||||
const isActive = location.pathname === to;
|
||||
return (
|
||||
<Link
|
||||
key={to}
|
||||
to={to}
|
||||
style={{
|
||||
padding: "0.5rem 1.25rem",
|
||||
borderRadius: "6px",
|
||||
textDecoration: "none",
|
||||
fontSize: "1.425rem",
|
||||
fontWeight: isActive ? 600 : 200,
|
||||
color: isActive ? "#fff" : "#999",
|
||||
backgroundColor: isActive ? "#333" : "transparent",
|
||||
transition: "all 0.15s ease",
|
||||
}}
|
||||
className={`navbar__link ${isActive ? "navbar__link--active" : ""}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
@@ -59,24 +34,9 @@ export default function Navbar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1.25rem" }}>
|
||||
<span style={{ fontSize: "1.275rem", color: "#777" }}>
|
||||
{currentUser.email}
|
||||
</span>
|
||||
<button
|
||||
onClick={logout}
|
||||
style={{
|
||||
padding: "0.4rem 1rem",
|
||||
fontSize: "1.275rem",
|
||||
fontWeight: 400,
|
||||
backgroundColor: "transparent",
|
||||
color: "#999",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "6px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<div className="flex flex--center gap-lg">
|
||||
<span className="navbar__user">{currentUser.email}</span>
|
||||
<button onClick={logout} className="navbar__logout">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap');
|
||||
|
||||
:root {
|
||||
--color-primary: #0066cc;
|
||||
--color-primary-hover: #0077ee;
|
||||
--color-brand: #e62525;
|
||||
--color-success: #28a745;
|
||||
--color-success-text: #00ff00;
|
||||
--color-success-bg: #00ff0020;
|
||||
--color-success-border: #00ff00;
|
||||
--color-error: #ff6666;
|
||||
--color-error-bg: #ff000020;
|
||||
--color-error-border: #ff0000;
|
||||
--color-warning: #ffcc44;
|
||||
--color-warning-bg: #3a2e00;
|
||||
--color-warning-border: #aa7700;
|
||||
--color-danger: #cc6666;
|
||||
--color-danger-border: #884444;
|
||||
|
||||
--color-bg: #1a1a1a;
|
||||
--color-bg-elevated: #242424;
|
||||
--color-bg-card: #333;
|
||||
--color-bg-section: #2a2a2a;
|
||||
|
||||
--color-border: #444;
|
||||
--color-border-light: #333;
|
||||
--color-border-section: #3a3a3a;
|
||||
|
||||
--color-text: white;
|
||||
--color-text-muted: #b3b3b3;
|
||||
--color-text-dimmed: #808080;
|
||||
--color-text-subtle: #999;
|
||||
--color-text-label: #ccc;
|
||||
|
||||
--radius-sm: 3px;
|
||||
--radius-md: 4px;
|
||||
--radius-lg: 6px;
|
||||
--radius-xl: 8px;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
@@ -31,3 +69,256 @@ input, select, textarea, button {
|
||||
font-size: 1rem;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
/* ── Page Layouts ─────────────────────────────────────────── */
|
||||
|
||||
.page {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.page--wide {
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
/* ── Cards & Sections ─────────────────────────────────────── */
|
||||
|
||||
.card {
|
||||
padding: 1rem;
|
||||
background-color: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-bg-section);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border-section);
|
||||
}
|
||||
|
||||
/* ── Form Controls ────────────────────────────────────────── */
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-field--last {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-label);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
background-color: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.25);
|
||||
}
|
||||
|
||||
.form-control--mono {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* ── Buttons ──────────────────────────────────────────────── */
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
color: var(--color-text);
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn--success {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
|
||||
.btn--secondary {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn--danger {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--color-danger-border);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.btn--ghost {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-subtle);
|
||||
}
|
||||
|
||||
.btn--sm {
|
||||
padding: 0.25rem 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn--lg {
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
/* ── Feedback Alerts ──────────────────────────────────────── */
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background-color: var(--color-error-bg);
|
||||
border: 1px solid var(--color-error-border);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
background-color: var(--color-success-bg);
|
||||
border: 1px solid var(--color-success-border);
|
||||
color: var(--color-success-text);
|
||||
}
|
||||
|
||||
.alert--warning {
|
||||
background-color: var(--color-warning-bg);
|
||||
border: 1px solid var(--color-warning-border);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
/* ── Lists ────────────────────────────────────────────────── */
|
||||
|
||||
.list-unstyled {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-item--card {
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
/* ── Typography Helpers ───────────────────────────────────── */
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.text-dimmed {
|
||||
color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
.text-mono {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 1.035rem;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.978rem;
|
||||
}
|
||||
|
||||
.help-link {
|
||||
color: var(--color-primary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ── Flex Utilities ───────────────────────────────────────── */
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex--between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex--center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex--column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex--wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.gap-xs {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.gap-sm {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-md {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.gap-lg {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ── Spacing Helpers ──────────────────────────────────────── */
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-sm { margin-bottom: 0.5rem; }
|
||||
.mb-md { margin-bottom: 1rem; }
|
||||
.mb-lg { margin-bottom: 1.5rem; }
|
||||
.mb-xl { margin-bottom: 2rem; }
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-md { margin-top: 1rem; }
|
||||
|
||||
.text-white { color: white; }
|
||||
.text-white-sm { color: white; font-size: 0.85rem; }
|
||||
|
||||
30
frontend/src/pages/Addresses.css
Normal file
30
frontend/src/pages/Addresses.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.address__edit-input {
|
||||
background: var(--color-border);
|
||||
border: 1px solid #666;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text);
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.address__edit-link {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6699cc;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.address__remove {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-danger-border);
|
||||
color: var(--color-danger);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-size: 0.85rem;
|
||||
margin-left: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import AddressForm from "../components/AddressForm";
|
||||
import { getAddresses, createAddress, deleteAddress, updateAddress } from "../api/addresses";
|
||||
import "./Addresses.css";
|
||||
|
||||
export default function Addresses() {
|
||||
const [addresses, setAddresses] = useState([]);
|
||||
@@ -9,7 +10,6 @@ export default function Addresses() {
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editLabel, setEditLabel] = useState("");
|
||||
|
||||
// Load addresses on mount
|
||||
useEffect(() => {
|
||||
async function fetchAddresses() {
|
||||
try {
|
||||
@@ -27,7 +27,6 @@ export default function Addresses() {
|
||||
fetchAddresses();
|
||||
}, []);
|
||||
|
||||
// Handle new address submission
|
||||
async function handleAddressSubmit(data) {
|
||||
try {
|
||||
const newAddress = await createAddress(data);
|
||||
@@ -78,51 +77,35 @@ export default function Addresses() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
|
||||
<h1>Tracked Addresses</h1>
|
||||
<div className="page">
|
||||
<h1>Add Addresses to Track</h1>
|
||||
|
||||
<div style={{ marginBottom: "2rem" }}>
|
||||
<div className="mb-xl">
|
||||
<AddressForm onSubmit={handleAddressSubmit} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Existing Addresses</h2>
|
||||
<h2>Existing Tracked Addresses</h2>
|
||||
{loading && <p>Loading addresses...</p>}
|
||||
{error && <p style={{ color: "red" }}>Error: {error}</p>}
|
||||
{error && <p className="text-error">Error: {error}</p>}
|
||||
{!loading && !error && addresses.length === 0 && (
|
||||
<p style={{ color: "#808080" }}>
|
||||
<p className="text-dimmed">
|
||||
No addresses tracked yet. Add one above to get started.
|
||||
</p>
|
||||
)}
|
||||
{addresses.length > 0 && (
|
||||
<ul style={{ listStyle: "none", padding: 0 }}>
|
||||
<ul className="list-unstyled">
|
||||
{addresses.map((addr, index) => (
|
||||
<li
|
||||
key={addr.id || index}
|
||||
style={{
|
||||
padding: "1rem",
|
||||
marginBottom: "0.5rem",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: "#333",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<li key={addr.id || index} className="list-item--card">
|
||||
<div className="flex flex--between flex--center">
|
||||
<div>
|
||||
{editingId === addr.id ? (
|
||||
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginBottom: "0.25rem" }}>
|
||||
<div className="flex flex--center gap-sm mb-sm">
|
||||
<input
|
||||
value={editLabel}
|
||||
onChange={(e) => setEditLabel(e.target.value)}
|
||||
placeholder="Label (optional)"
|
||||
style={{
|
||||
background: "#444",
|
||||
border: "1px solid #666",
|
||||
borderRadius: "3px",
|
||||
color: "#fff",
|
||||
padding: "0.25rem 0.5rem",
|
||||
fontSize: "0.9rem",
|
||||
}}
|
||||
className="address__edit-input"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleEditSave(addr.id);
|
||||
if (e.key === "Escape") handleEditCancel();
|
||||
@@ -131,61 +114,37 @@ export default function Addresses() {
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleEditSave(addr.id)}
|
||||
style={{ cursor: "pointer", padding: "0.25rem 0.6rem", fontSize: "0.85rem" }}
|
||||
className="btn btn--primary btn--sm"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEditCancel}
|
||||
style={{ cursor: "pointer", padding: "0.25rem 0.6rem", fontSize: "0.85rem", background: "transparent", color: "#aaa", border: "1px solid #555" }}
|
||||
className="btn btn--ghost btn--sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.25rem" }}>
|
||||
<span style={{ fontWeight: "bold" }}>
|
||||
<div className="flex flex--center gap-sm mb-sm">
|
||||
<span className="text-bold">
|
||||
{addr.label || "Unlabeled"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleEditStart(addr)}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "#6699cc",
|
||||
fontSize: "0.8rem",
|
||||
padding: "0",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
className="address__edit-link"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "1.035rem",
|
||||
color: "#b3b3b3",
|
||||
}}
|
||||
>
|
||||
<div className="text-mono text-sm text-muted">
|
||||
{addr.address}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDelete(addr.id, addr.label)}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
background: "transparent",
|
||||
border: "1px solid #884444",
|
||||
color: "#cc6666",
|
||||
borderRadius: "3px",
|
||||
padding: "0.3rem 0.7rem",
|
||||
fontSize: "0.85rem",
|
||||
marginLeft: "1rem",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
className="address__remove"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
|
||||
7
frontend/src/pages/AlertHistory.css
Normal file
7
frontend/src/pages/AlertHistory.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.alert-history__item {
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { getAlertEvents } from "../api/alertEvents";
|
||||
import "./AlertHistory.css";
|
||||
|
||||
export default function AlertHistory() {
|
||||
const [alertEvents, setAlertEvents] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Fetch alert events on mount
|
||||
useEffect(() => {
|
||||
async function fetchAlertEvents() {
|
||||
try {
|
||||
@@ -25,49 +25,32 @@ export default function AlertHistory() {
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ padding: "2rem" }}>Loading...</div>;
|
||||
return <div className="page">Loading...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: "2rem", color: "red" }}>Error: {error}</div>
|
||||
<div className="page text-error">Error: {error}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
|
||||
<div className="page">
|
||||
<h1>Recent Alert Events</h1>
|
||||
|
||||
{alertEvents.length === 0 ? (
|
||||
<p style={{ color: "#808080" }}>No alerts yet</p>
|
||||
<p className="text-dimmed">No alerts yet</p>
|
||||
) : (
|
||||
<ul style={{ listStyle: "none", padding: 0 }}>
|
||||
<ul className="list-unstyled">
|
||||
{alertEvents.map((event) => (
|
||||
<li
|
||||
key={event.id}
|
||||
style={{
|
||||
padding: "1rem",
|
||||
marginBottom: "0.75rem",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: "#333",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "0.5rem" }}>
|
||||
{event.message}
|
||||
</div>
|
||||
<li key={event.id} className="alert-history__item">
|
||||
<div className="mb-sm">{event.message}</div>
|
||||
{event.address_label && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "1.035rem",
|
||||
color: "#808080",
|
||||
marginBottom: "0.25rem",
|
||||
}}
|
||||
>
|
||||
<div className="text-sm text-dimmed">
|
||||
Address: {event.address_label}
|
||||
</div>
|
||||
)}
|
||||
<small style={{ color: "#b3b3b3" }}>
|
||||
<small className="text-muted">
|
||||
{formatTimestamp(event.timestamp)}
|
||||
</small>
|
||||
</li>
|
||||
@@ -78,7 +61,6 @@ export default function AlertHistory() {
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to format timestamp for display
|
||||
function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
|
||||
63
frontend/src/pages/Alerts.css
Normal file
63
frontend/src/pages/Alerts.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.alerts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.alerts__address-info {
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.alerts__toggle-panel {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.alerts__toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.alerts__toggle-checkbox {
|
||||
margin-right: 0.5rem;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.alerts__toggle-hint {
|
||||
font-size: 0.978rem;
|
||||
color: var(--color-text-dimmed);
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 26px;
|
||||
}
|
||||
|
||||
.alerts__rule {
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.alerts__rule--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.alerts__email-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.alerts__save-test {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import AlertForm from "../components/AlertForm";
|
||||
import Button from "../components/Button";
|
||||
import Input from "../components/Input";
|
||||
import { getAddresses } from "../api/addresses";
|
||||
import {
|
||||
getAlerts,
|
||||
@@ -15,35 +16,7 @@ import {
|
||||
setupEmail,
|
||||
sendEmailDigest,
|
||||
} from "../api/notificationConfig";
|
||||
|
||||
const inputStyle = {
|
||||
width: "100%",
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.9rem",
|
||||
backgroundColor: "#1a1a1a",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
color: "white",
|
||||
fontFamily: "monospace",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
|
||||
const labelStyle = {
|
||||
display: "block",
|
||||
marginBottom: "0.25rem",
|
||||
fontSize: "0.9rem",
|
||||
color: "#ccc",
|
||||
};
|
||||
|
||||
const helpLinkStyle = { color: "#0066cc", fontSize: "0.85rem" };
|
||||
|
||||
const sectionStyle = {
|
||||
marginBottom: "1.5rem",
|
||||
padding: "1rem",
|
||||
backgroundColor: "#2a2a2a",
|
||||
borderRadius: "6px",
|
||||
border: "1px solid #3a3a3a",
|
||||
};
|
||||
import "./Alerts.css";
|
||||
|
||||
export default function Alerts() {
|
||||
const [addresses, setAddresses] = useState([]);
|
||||
@@ -52,7 +25,6 @@ export default function Alerts() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Notification config state
|
||||
const [notificationEnabled, setNotificationEnabled] = useState(false);
|
||||
const [discordWebhookUrl, setDiscordWebhookUrl] = useState("");
|
||||
const [telegramBotToken, setTelegramBotToken] = useState("");
|
||||
@@ -273,42 +245,28 @@ export default function Alerts() {
|
||||
const selectedAddress = addresses.find((a) => a.id === selectedAddressId);
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ padding: "2rem" }}>Loading addresses...</div>;
|
||||
return <div className="page">Loading addresses...</div>;
|
||||
}
|
||||
|
||||
if (addresses.length === 0) {
|
||||
return (
|
||||
<div style={{ padding: "2rem" }}>
|
||||
<p>
|
||||
No addresses tracked yet. Add an address first to create
|
||||
alerts.
|
||||
</p>
|
||||
<div className="page">
|
||||
<p>No addresses tracked yet. Add an address first to create alerts.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "1400px", margin: "0 auto", padding: "2rem" }}>
|
||||
<h1 style={{ marginBottom: "2rem" }}>
|
||||
Alert Rules & Notifications
|
||||
</h1>
|
||||
<div className="page page--wide">
|
||||
<h1 className="mb-xl">Alert Rules & Notifications</h1>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: "2rem",
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
<div className="alerts-grid">
|
||||
{/* LEFT COLUMN: Alert Rules */}
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>Alert Rules</h2>
|
||||
<h2 className="mt-0">Alert Rules</h2>
|
||||
|
||||
<div style={{ marginBottom: "2rem" }}>
|
||||
<label
|
||||
style={{ display: "block", marginBottom: "0.5rem" }}
|
||||
>
|
||||
<div className="mb-xl">
|
||||
<label className="form-label mb-sm">
|
||||
<strong>Select Address:</strong>
|
||||
</label>
|
||||
<select
|
||||
@@ -316,15 +274,7 @@ export default function Alerts() {
|
||||
onChange={(e) =>
|
||||
setSelectedAddressId(Number(e.target.value))
|
||||
}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
backgroundColor: "#1a1a1a",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
color: "white",
|
||||
}}
|
||||
className="form-select"
|
||||
>
|
||||
{addresses.map((addr) => (
|
||||
<option key={addr.id} value={addr.id}>
|
||||
@@ -336,43 +286,19 @@ export default function Alerts() {
|
||||
|
||||
{selectedAddress && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
padding: "1rem",
|
||||
marginBottom: "2rem",
|
||||
backgroundColor: "#333",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #444",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "1.035rem",
|
||||
color: "#b3b3b3",
|
||||
}}
|
||||
>
|
||||
<div className="alerts__address-info">
|
||||
<div className="text-sm text-muted">
|
||||
Managing alerts for:
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
marginTop: "0.25rem",
|
||||
}}
|
||||
>
|
||||
<div className="text-bold">
|
||||
{selectedAddress.label || "Unlabeled"}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "1.035rem",
|
||||
color: "#b3b3b3",
|
||||
}}
|
||||
>
|
||||
<div className="text-mono text-sm text-muted">
|
||||
{selectedAddress.address}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "2rem" }}>
|
||||
<div className="mb-xl">
|
||||
<h3>Create New Alert</h3>
|
||||
<AlertForm onSubmit={handleAlertSubmit} />
|
||||
</div>
|
||||
@@ -380,112 +306,43 @@ export default function Alerts() {
|
||||
<div>
|
||||
<h3>Active Alert Rules</h3>
|
||||
{error && (
|
||||
<p style={{ color: "red" }}>
|
||||
Error: {error}
|
||||
</p>
|
||||
<p className="text-error">Error: {error}</p>
|
||||
)}
|
||||
{alerts.length === 0 ? (
|
||||
<p style={{ color: "#666" }}>
|
||||
No alert rules defined yet. Create one
|
||||
above.
|
||||
<p className="text-dimmed">
|
||||
No alert rules defined yet. Create one above.
|
||||
</p>
|
||||
) : (
|
||||
<ul
|
||||
style={{
|
||||
listStyle: "none",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<ul className="list-unstyled">
|
||||
{alerts.map((alert) => (
|
||||
<li
|
||||
key={alert.id}
|
||||
style={{
|
||||
padding: "1rem",
|
||||
marginBottom: "0.5rem",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: "#333",
|
||||
opacity: alert.enabled
|
||||
? 1
|
||||
: 0.6,
|
||||
}}
|
||||
className={`alerts__rule ${!alert.enabled ? "alerts__rule--disabled" : ""}`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent:
|
||||
"space-between",
|
||||
alignItems:
|
||||
"flex-start",
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight:
|
||||
"bold",
|
||||
marginBottom:
|
||||
"0.25rem",
|
||||
}}
|
||||
>
|
||||
{formatAlertType(
|
||||
alert.type,
|
||||
)}
|
||||
<div className="flex flex--between">
|
||||
<div>
|
||||
<div className="text-bold mb-sm">
|
||||
{formatAlertType(alert.type)}
|
||||
</div>
|
||||
{alert.threshold && (
|
||||
<div
|
||||
style={{
|
||||
fontSize:
|
||||
"1.035rem",
|
||||
color: "#b3b3b3",
|
||||
}}
|
||||
>
|
||||
Threshold:{" "}
|
||||
{
|
||||
alert.threshold
|
||||
}{" "}
|
||||
ETH
|
||||
<div className="text-sm text-muted">
|
||||
Threshold: {alert.threshold} ETH
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontSize:
|
||||
"0.978rem",
|
||||
color: "#808080",
|
||||
marginTop:
|
||||
"0.25rem",
|
||||
}}
|
||||
>
|
||||
Status:{" "}
|
||||
{alert.enabled
|
||||
? "Enabled"
|
||||
: "Disabled"}
|
||||
<div className="text-xs text-dimmed">
|
||||
Status: {alert.enabled ? "Enabled" : "Disabled"}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<div className="flex flex--center gap-sm">
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleToggleAlert(
|
||||
alert.id,
|
||||
alert.enabled,
|
||||
)
|
||||
handleToggleAlert(alert.id, alert.enabled)
|
||||
}
|
||||
>
|
||||
{alert.enabled
|
||||
? "Disable"
|
||||
: "Enable"}
|
||||
{alert.enabled ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleDeleteAlert(
|
||||
alert.id,
|
||||
)
|
||||
}
|
||||
onClick={() => handleDeleteAlert(alert.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@@ -502,329 +359,152 @@ export default function Alerts() {
|
||||
|
||||
{/* RIGHT COLUMN: Notification Settings */}
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>Notification Settings</h2>
|
||||
<h2 className="mt-0">Notification Settings</h2>
|
||||
|
||||
{notificationSuccess && (
|
||||
<div
|
||||
style={{
|
||||
padding: "0.75rem",
|
||||
marginBottom: "1rem",
|
||||
backgroundColor: "#00ff0020",
|
||||
border: "1px solid #00ff00",
|
||||
borderRadius: "4px",
|
||||
color: "#00ff00",
|
||||
}}
|
||||
>
|
||||
{notificationSuccess}
|
||||
</div>
|
||||
<div className="alert alert--success">{notificationSuccess}</div>
|
||||
)}
|
||||
|
||||
{notificationError && (
|
||||
<div
|
||||
style={{
|
||||
padding: "0.75rem",
|
||||
marginBottom: "1rem",
|
||||
backgroundColor: "#ff000020",
|
||||
border: "1px solid #ff0000",
|
||||
borderRadius: "4px",
|
||||
color: "#ff6666",
|
||||
}}
|
||||
>
|
||||
{notificationError}
|
||||
</div>
|
||||
<div className="alert alert--error">{notificationError}</div>
|
||||
)}
|
||||
|
||||
{/* Master toggle */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "1.5rem",
|
||||
padding: "1rem",
|
||||
backgroundColor: "#333",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div className="alerts__toggle-panel">
|
||||
<label className="alerts__toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationEnabled}
|
||||
onChange={(e) =>
|
||||
setNotificationEnabled(e.target.checked)
|
||||
}
|
||||
style={{
|
||||
marginRight: "0.5rem",
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
}}
|
||||
className="alerts__toggle-checkbox"
|
||||
/>
|
||||
<span style={{ fontWeight: "bold" }}>
|
||||
Enable Notifications
|
||||
</span>
|
||||
<span className="text-bold">Enable Notifications</span>
|
||||
</label>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.978rem",
|
||||
color: "#808080",
|
||||
marginTop: "0.5rem",
|
||||
marginLeft: "26px",
|
||||
}}
|
||||
>
|
||||
<div className="alerts__toggle-hint">
|
||||
Master switch for all notification channels
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* All channel settings — hidden when master toggle is off */}
|
||||
<div
|
||||
style={{
|
||||
display: notificationEnabled ? "block" : "none",
|
||||
}}
|
||||
>
|
||||
{/* Telegram */}
|
||||
<div style={sectionStyle}>
|
||||
<h3 style={{ marginTop: 0, marginBottom: "1rem" }}>
|
||||
Telegram
|
||||
</h3>
|
||||
|
||||
<div style={{ marginBottom: "0.75rem" }}>
|
||||
<label style={labelStyle}>Bot Token</label>
|
||||
<input
|
||||
type="text"
|
||||
{/* All channel settings -- hidden when master toggle is off */}
|
||||
{notificationEnabled && (
|
||||
<>
|
||||
{/* Telegram */}
|
||||
<div className="section">
|
||||
<h3 className="mt-0 mb-md">Telegram</h3>
|
||||
<Input
|
||||
label="Bot Token"
|
||||
value={telegramBotToken}
|
||||
onChange={(e) =>
|
||||
setTelegramBotToken(e.target.value)
|
||||
}
|
||||
onChange={setTelegramBotToken}
|
||||
placeholder="123456789:ABCdefGHIjklMNOpqrSTUvwxYZ"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "0.5rem" }}>
|
||||
<label style={labelStyle}>Chat ID</label>
|
||||
<input
|
||||
type="text"
|
||||
<Input
|
||||
label="Chat ID"
|
||||
value={telegramChatId}
|
||||
onChange={(e) =>
|
||||
setTelegramChatId(e.target.value)
|
||||
}
|
||||
onChange={setTelegramChatId}
|
||||
placeholder="-1001234567890"
|
||||
style={inputStyle}
|
||||
/>
|
||||
<a
|
||||
href="https://core.telegram.org/bots#how-do-i-create-a-bot"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="help-link"
|
||||
>
|
||||
How to create a Telegram bot & get your Chat ID
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://core.telegram.org/bots#how-do-i-create-a-bot"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={helpLinkStyle}
|
||||
>
|
||||
How to create a Telegram bot & get your Chat ID
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div style={sectionStyle}>
|
||||
<h3 style={{ marginTop: 0, marginBottom: "1rem" }}>
|
||||
Email
|
||||
</h3>
|
||||
|
||||
<div style={{ marginBottom: "0.75rem" }}>
|
||||
<label style={labelStyle}>Email Address</label>
|
||||
<input
|
||||
{/* Email */}
|
||||
<div className="section">
|
||||
<h3 className="mt-0 mb-md">Email</h3>
|
||||
<Input
|
||||
label="Email Address"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onChange={setEmail}
|
||||
placeholder="you@example.com"
|
||||
style={inputStyle}
|
||||
/>
|
||||
<div className="help-link text-dimmed mb-md">
|
||||
Alert notifications and digests will be sent to this address
|
||||
</div>
|
||||
<div className="alerts__email-buttons">
|
||||
<Button
|
||||
onClick={handleSetupEmail}
|
||||
disabled={settingUpEmail || !email}
|
||||
className="btn--sm"
|
||||
>
|
||||
{settingUpEmail ? "Setting up..." : "Verify Email"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSendDigest}
|
||||
disabled={sendingDigest || !email}
|
||||
variant="secondary"
|
||||
className="btn--sm"
|
||||
>
|
||||
{sendingDigest ? "Sending..." : "Send Digest Now"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85rem",
|
||||
color: "#808080",
|
||||
marginBottom: "0.75rem",
|
||||
}}
|
||||
>
|
||||
Alert notifications and digests will be sent to
|
||||
this address
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={handleSetupEmail}
|
||||
disabled={settingUpEmail || !email}
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
fontSize: "0.85rem",
|
||||
backgroundColor:
|
||||
settingUpEmail || !email
|
||||
? "#333"
|
||||
: "#0066cc",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor:
|
||||
settingUpEmail || !email
|
||||
? "not-allowed"
|
||||
: "pointer",
|
||||
}}
|
||||
>
|
||||
{settingUpEmail
|
||||
? "Setting up..."
|
||||
: "Verify Email"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSendDigest}
|
||||
disabled={sendingDigest || !email}
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
fontSize: "0.85rem",
|
||||
backgroundColor:
|
||||
sendingDigest || !email
|
||||
? "#333"
|
||||
: "#6c757d",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor:
|
||||
sendingDigest || !email
|
||||
? "not-allowed"
|
||||
: "pointer",
|
||||
}}
|
||||
>
|
||||
{sendingDigest
|
||||
? "Sending..."
|
||||
: "Send Digest Now"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Discord */}
|
||||
<div style={sectionStyle}>
|
||||
<h3 style={{ marginTop: 0, marginBottom: "1rem" }}>
|
||||
Discord
|
||||
</h3>
|
||||
|
||||
<div style={{ marginBottom: "0.5rem" }}>
|
||||
<label style={labelStyle}>
|
||||
Discord Webhook URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
{/* Discord */}
|
||||
<div className="section">
|
||||
<h3 className="mt-0 mb-md">Discord</h3>
|
||||
<Input
|
||||
label="Discord Webhook URL"
|
||||
value={discordWebhookUrl}
|
||||
onChange={(e) =>
|
||||
setDiscordWebhookUrl(e.target.value)
|
||||
}
|
||||
onChange={setDiscordWebhookUrl}
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
style={inputStyle}
|
||||
/>
|
||||
<a
|
||||
href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="help-link"
|
||||
>
|
||||
How to get a Discord webhook URL
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={helpLinkStyle}
|
||||
>
|
||||
How to get a Discord webhook URL
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Slack */}
|
||||
<div style={sectionStyle}>
|
||||
<h3 style={{ marginTop: 0, marginBottom: "1rem" }}>
|
||||
Slack
|
||||
</h3>
|
||||
|
||||
<div style={{ marginBottom: "0.5rem" }}>
|
||||
<label style={labelStyle}>
|
||||
Slack Webhook URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
{/* Slack */}
|
||||
<div className="section">
|
||||
<h3 className="mt-0 mb-md">Slack</h3>
|
||||
<Input
|
||||
label="Slack Webhook URL"
|
||||
value={slackWebhookUrl}
|
||||
onChange={(e) =>
|
||||
setSlackWebhookUrl(e.target.value)
|
||||
}
|
||||
onChange={setSlackWebhookUrl}
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
style={inputStyle}
|
||||
/>
|
||||
<a
|
||||
href="https://api.slack.com/messaging/webhooks"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="help-link"
|
||||
>
|
||||
How to set up Slack Incoming Webhooks
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://api.slack.com/messaging/webhooks"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={helpLinkStyle}
|
||||
>
|
||||
How to set up Slack Incoming Webhooks
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Save & Test buttons at the bottom */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.75rem",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={handleSaveNotificationConfig}
|
||||
disabled={notificationLoading}
|
||||
style={{
|
||||
padding: "0.75rem 1.5rem",
|
||||
fontSize: "1rem",
|
||||
backgroundColor: notificationLoading
|
||||
? "#333"
|
||||
: "#0066cc",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: notificationLoading
|
||||
? "not-allowed"
|
||||
: "pointer",
|
||||
}}
|
||||
>
|
||||
{notificationLoading
|
||||
? "Saving..."
|
||||
: "Save Settings"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleTestChannels}
|
||||
disabled={testingChannels}
|
||||
style={{
|
||||
padding: "0.75rem 1.5rem",
|
||||
fontSize: "1rem",
|
||||
backgroundColor: testingChannels
|
||||
? "#333"
|
||||
: "#28a745",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: testingChannels
|
||||
? "not-allowed"
|
||||
: "pointer",
|
||||
}}
|
||||
>
|
||||
{testingChannels
|
||||
? "Testing..."
|
||||
: "Test All Channels"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Save & Test buttons */}
|
||||
<div className="alerts__save-test">
|
||||
<Button
|
||||
onClick={handleSaveNotificationConfig}
|
||||
disabled={notificationLoading}
|
||||
className="btn--lg"
|
||||
>
|
||||
{notificationLoading ? "Saving..." : "Save Settings"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleTestChannels}
|
||||
disabled={testingChannels}
|
||||
variant="success"
|
||||
className="btn--lg"
|
||||
>
|
||||
{testingChannels ? "Testing..." : "Test All Channels"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
margin: 20px auto;
|
||||
padding: 2rem;
|
||||
padding-top: 8rem;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-xl);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
@@ -22,55 +22,13 @@
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
color: #e62525;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
background-color: #ff000020;
|
||||
border: 1px solid #ff0000;
|
||||
border-radius: 4px;
|
||||
color: #ff6666;
|
||||
}
|
||||
|
||||
.login-field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-field-last {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.login-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
background-color: #242424;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.2rem;
|
||||
background-color: #0066cc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
background-color: #333;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
@@ -78,22 +36,14 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-footer-text {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.login-signup-link {
|
||||
color: #0066cc;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.login-form-hidden {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import Input from "../components/Input";
|
||||
import "./Login.css";
|
||||
|
||||
export default function Login() {
|
||||
@@ -16,7 +17,6 @@ export default function Login() {
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Validation
|
||||
if (!email || !password) {
|
||||
setError("Please fill in all fields");
|
||||
return;
|
||||
@@ -26,7 +26,7 @@ export default function Login() {
|
||||
setError("");
|
||||
setLoading(true);
|
||||
await login(email, password);
|
||||
navigate("/addresses"); // Redirect to main app
|
||||
navigate("/addresses");
|
||||
} catch (err) {
|
||||
setError("Failed to log in: " + err.message);
|
||||
} finally {
|
||||
@@ -36,66 +36,42 @@ export default function Login() {
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div
|
||||
className="login-card"
|
||||
onMouseEnter={() => setIsVisible(true)}
|
||||
>
|
||||
<div className="login-card" onMouseEnter={() => setIsVisible(true)}>
|
||||
<h1 className="login-heading">
|
||||
<span className="login-brand">Koin Ping</span> - Login
|
||||
</h1>
|
||||
|
||||
<div className={isVisible ? 'login-form-visible' : 'login-form-hidden'}>
|
||||
{error && (
|
||||
<div className="login-error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className={isVisible ? "login-form-visible" : "login-form-hidden"}>
|
||||
{error && <div className="alert alert--error">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="login-field">
|
||||
<label className="login-label">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={loading}
|
||||
className="login-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="login-field-last">
|
||||
<label className="login-label">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={loading}
|
||||
className="login-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
disabled={loading}
|
||||
className="login-button"
|
||||
>
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={setPassword}
|
||||
disabled={loading}
|
||||
required
|
||||
className="form-field--last"
|
||||
/>
|
||||
|
||||
<button type="submit" disabled={loading} className="btn btn--primary login-button">
|
||||
{loading ? "Logging in..." : "Log In"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="login-footer">
|
||||
<p className="login-footer-text">
|
||||
<p className="text-muted">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
to="/signup"
|
||||
className="login-signup-link"
|
||||
>
|
||||
<Link to="/signup" className="login-signup-link">
|
||||
Sign up here
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
185
frontend/src/pages/Onboarding.css
Normal file
185
frontend/src/pages/Onboarding.css
Normal file
@@ -0,0 +1,185 @@
|
||||
.onboarding {
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
.onboarding__container {
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.onboarding__title {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.onboarding__card {
|
||||
background-color: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.onboarding__login-link {
|
||||
text-align: center;
|
||||
margin-top: 1.25rem;
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.onboarding__login-link a {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-bar__step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-bar__connector {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.progress-bar__connector--active {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.progress-bar__connector--inactive {
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
.progress-bar__dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
color: white;
|
||||
margin: 0 auto 4px;
|
||||
}
|
||||
|
||||
.progress-bar__dot--done,
|
||||
.progress-bar__dot--active {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.progress-bar__dot--active {
|
||||
border: 2px solid #4499ff;
|
||||
}
|
||||
|
||||
.progress-bar__dot--pending {
|
||||
background-color: var(--color-bg-card);
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.progress-bar__label {
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress-bar__label--active {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.progress-bar__label--inactive {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Step 5 summary */
|
||||
.onboarding__summary {
|
||||
background-color: #1e2e1e;
|
||||
border: 1px solid #2d5a2d;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.onboarding__summary-title {
|
||||
margin: 0 0 0.5rem;
|
||||
color: #90ee90;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.onboarding__summary-list {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
color: var(--color-text-label);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* Checkbox rows */
|
||||
.checkbox-row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.checkbox-row__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
cursor: pointer;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.checkbox-row__input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.checkbox-row__nested {
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 1.75rem;
|
||||
}
|
||||
|
||||
/* Footer navigation */
|
||||
.onboarding__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
/* Test results */
|
||||
.test-result {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.test-result--success {
|
||||
color: #90ee90;
|
||||
}
|
||||
|
||||
.test-result--failure {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Step subtitle */
|
||||
.onboarding__subtitle {
|
||||
color: #aaa;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Onboarding Wizard
|
||||
*
|
||||
* 5-step guided flow: Create Account → Add Wallet → Alert Rules → Notifications → Done
|
||||
* 5-step guided flow: Create Account -> Add Wallet -> Alert Rules -> Notifications -> Done
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
updateNotificationConfig,
|
||||
testNotificationChannels,
|
||||
} from "../api/notificationConfig";
|
||||
import Input from "../components/Input";
|
||||
import Button from "../components/Button";
|
||||
import "./Onboarding.css";
|
||||
|
||||
const STEPS = [
|
||||
"Create Account",
|
||||
@@ -22,24 +25,6 @@ const STEPS = [
|
||||
"Done",
|
||||
];
|
||||
|
||||
const inputStyle = {
|
||||
width: "100%",
|
||||
padding: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
backgroundColor: "#2a2a2a",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
color: "white",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
|
||||
const labelStyle = {
|
||||
display: "block",
|
||||
marginBottom: "0.4rem",
|
||||
color: "#ccc",
|
||||
fontSize: "0.9rem",
|
||||
};
|
||||
|
||||
export default function Onboarding() {
|
||||
const { currentUser, signup } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
@@ -51,7 +36,6 @@ export default function Onboarding() {
|
||||
const [testResults, setTestResults] = useState(null);
|
||||
const [testLoading, setTestLoading] = useState(false);
|
||||
|
||||
// Wizard state
|
||||
const [data, setData] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
@@ -68,7 +52,6 @@ export default function Onboarding() {
|
||||
discordWebhookUrl: "",
|
||||
slackWebhookUrl: "",
|
||||
notificationEmail: "",
|
||||
// summary
|
||||
alertsCreated: [],
|
||||
notificationConfigured: false,
|
||||
});
|
||||
@@ -77,7 +60,6 @@ export default function Onboarding() {
|
||||
setData((prev) => ({ ...prev, [field]: value }));
|
||||
}
|
||||
|
||||
// On mount: if already fully onboarded, redirect away
|
||||
useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
getAddresses()
|
||||
@@ -86,7 +68,7 @@ export default function Onboarding() {
|
||||
navigate("/addresses", { replace: true });
|
||||
}
|
||||
})
|
||||
.catch(() => {}); // ignore errors (e.g. mid-signup)
|
||||
.catch(() => {});
|
||||
}, [currentUser, navigate]);
|
||||
|
||||
// ── Step handlers ─────────────────────────────────────────────────────────
|
||||
@@ -105,7 +87,6 @@ export default function Onboarding() {
|
||||
setError("Password must be at least 6 characters");
|
||||
return;
|
||||
}
|
||||
// If user already exists (browser-close-mid-wizard), skip signup
|
||||
if (!currentUser) {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -237,59 +218,38 @@ export default function Onboarding() {
|
||||
|
||||
function ProgressBar() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: "2rem",
|
||||
}}
|
||||
>
|
||||
<div className="progress-bar">
|
||||
{STEPS.map((label, i) => {
|
||||
const stepNum = i + 1;
|
||||
const done = step > stepNum;
|
||||
const active = step === stepNum;
|
||||
const dotClass = done
|
||||
? "progress-bar__dot--done"
|
||||
: active
|
||||
? "progress-bar__dot--active"
|
||||
: "progress-bar__dot--pending";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={label}
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<div key={label} className="progress-bar__step">
|
||||
{i > 0 && (
|
||||
<div
|
||||
style={{
|
||||
width: "40px",
|
||||
height: "2px",
|
||||
backgroundColor: done || active ? "#0066cc" : "#444",
|
||||
margin: "0 4px",
|
||||
}}
|
||||
className={`progress-bar__connector ${
|
||||
done || active
|
||||
? "progress-bar__connector--active"
|
||||
: "progress-bar__connector--inactive"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor:
|
||||
done ? "#0066cc" : active ? "#0066cc" : "#333",
|
||||
border: active ? "2px solid #4499ff" : "2px solid transparent",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontWeight: "bold",
|
||||
fontSize: "0.85rem",
|
||||
color: "white",
|
||||
margin: "0 auto 4px",
|
||||
}}
|
||||
>
|
||||
{done ? "✓" : stepNum}
|
||||
<div>
|
||||
<div className={`progress-bar__dot ${dotClass}`}>
|
||||
{done ? "\u2713" : stepNum}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
color: active ? "white" : "#888",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
className={`progress-bar__label ${
|
||||
active
|
||||
? "progress-bar__label--active"
|
||||
: "progress-bar__label--inactive"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
@@ -306,40 +266,32 @@ export default function Onboarding() {
|
||||
function Step1() {
|
||||
return (
|
||||
<>
|
||||
<h2 style={{ marginBottom: "1.5rem" }}>Create your account</h2>
|
||||
<div style={{ marginBottom: "1rem" }}>
|
||||
<label style={labelStyle}>Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => set("email", e.target.value)}
|
||||
disabled={loading}
|
||||
style={inputStyle}
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: "1rem" }}>
|
||||
<label style={labelStyle}>Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={data.password}
|
||||
onChange={(e) => set("password", e.target.value)}
|
||||
disabled={loading}
|
||||
style={inputStyle}
|
||||
placeholder="At least 6 characters"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={labelStyle}>Confirm Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={data.confirmPassword}
|
||||
onChange={(e) => set("confirmPassword", e.target.value)}
|
||||
disabled={loading}
|
||||
style={inputStyle}
|
||||
placeholder="Repeat your password"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="mb-lg">Create your account</h2>
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(v) => set("email", v)}
|
||||
disabled={loading}
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
value={data.password}
|
||||
onChange={(v) => set("password", v)}
|
||||
disabled={loading}
|
||||
placeholder="At least 6 characters"
|
||||
/>
|
||||
<Input
|
||||
label="Confirm Password"
|
||||
type="password"
|
||||
value={data.confirmPassword}
|
||||
onChange={(v) => set("confirmPassword", v)}
|
||||
disabled={loading}
|
||||
placeholder="Repeat your password"
|
||||
className="form-field--last"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -347,32 +299,25 @@ export default function Onboarding() {
|
||||
function Step2() {
|
||||
return (
|
||||
<>
|
||||
<h2 style={{ marginBottom: "0.5rem" }}>Add a wallet address</h2>
|
||||
<p style={{ color: "#aaa", marginBottom: "1.5rem", fontSize: "0.9rem" }}>
|
||||
<h2 className="mb-sm">Add a wallet address</h2>
|
||||
<p className="onboarding__subtitle">
|
||||
Enter the Ethereum address you want to monitor.
|
||||
</p>
|
||||
<div style={{ marginBottom: "1rem" }}>
|
||||
<label style={labelStyle}>ETH Address</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.walletAddress}
|
||||
onChange={(e) => set("walletAddress", e.target.value)}
|
||||
disabled={loading}
|
||||
style={inputStyle}
|
||||
placeholder="0x..."
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={labelStyle}>Label (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.walletLabel}
|
||||
onChange={(e) => set("walletLabel", e.target.value)}
|
||||
disabled={loading}
|
||||
style={inputStyle}
|
||||
placeholder="e.g. My main wallet"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="ETH Address"
|
||||
value={data.walletAddress}
|
||||
onChange={(v) => set("walletAddress", v)}
|
||||
disabled={loading}
|
||||
placeholder="0x..."
|
||||
/>
|
||||
<Input
|
||||
label="Label (optional)"
|
||||
value={data.walletLabel}
|
||||
onChange={(v) => set("walletLabel", v)}
|
||||
disabled={loading}
|
||||
placeholder="e.g. My main wallet"
|
||||
className="form-field--last"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -380,8 +325,8 @@ export default function Onboarding() {
|
||||
function Step3() {
|
||||
return (
|
||||
<>
|
||||
<h2 style={{ marginBottom: "0.5rem" }}>Configure alert rules</h2>
|
||||
<p style={{ color: "#aaa", marginBottom: "1.5rem", fontSize: "0.9rem" }}>
|
||||
<h2 className="mb-sm">Configure alert rules</h2>
|
||||
<p className="onboarding__subtitle">
|
||||
Choose which events trigger notifications. You can change these later.
|
||||
</p>
|
||||
|
||||
@@ -401,12 +346,12 @@ export default function Onboarding() {
|
||||
label="Large transfer"
|
||||
>
|
||||
{data.alertLargeTransfer && (
|
||||
<div style={{ marginTop: "0.5rem", marginLeft: "1.75rem" }}>
|
||||
<input
|
||||
<div className="checkbox-row__nested">
|
||||
<Input
|
||||
type="number"
|
||||
label=""
|
||||
value={data.largeTransferThreshold}
|
||||
onChange={(e) => set("largeTransferThreshold", e.target.value)}
|
||||
style={{ ...inputStyle, width: "160px" }}
|
||||
onChange={(v) => set("largeTransferThreshold", v)}
|
||||
placeholder="Threshold (ETH)"
|
||||
min="0"
|
||||
step="0.01"
|
||||
@@ -420,12 +365,12 @@ export default function Onboarding() {
|
||||
label="Balance below"
|
||||
>
|
||||
{data.alertBalanceBelow && (
|
||||
<div style={{ marginTop: "0.5rem", marginLeft: "1.75rem" }}>
|
||||
<input
|
||||
<div className="checkbox-row__nested">
|
||||
<Input
|
||||
type="number"
|
||||
label=""
|
||||
value={data.balanceBelowThreshold}
|
||||
onChange={(e) => set("balanceBelowThreshold", e.target.value)}
|
||||
style={{ ...inputStyle, width: "160px" }}
|
||||
onChange={(v) => set("balanceBelowThreshold", v)}
|
||||
placeholder="Threshold (ETH)"
|
||||
min="0"
|
||||
step="0.01"
|
||||
@@ -440,66 +385,64 @@ export default function Onboarding() {
|
||||
function Step4() {
|
||||
return (
|
||||
<>
|
||||
<h2 style={{ marginBottom: "0.5rem" }}>Set up notifications</h2>
|
||||
<p style={{ color: "#aaa", marginBottom: "1.5rem", fontSize: "0.9rem" }}>
|
||||
<h2 className="mb-sm">Set up notifications</h2>
|
||||
<p className="onboarding__subtitle">
|
||||
Add at least one channel so you receive alerts. All fields are optional.
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: "1.25rem" }}>
|
||||
<label style={labelStyle}>
|
||||
<div className="mb-md">
|
||||
<label className="form-label">
|
||||
Discord Webhook URL{" "}
|
||||
<a
|
||||
href="https://support.discord.com/hc/en-us/articles/228383668"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: "#4499ff", fontSize: "0.8rem" }}
|
||||
className="help-link"
|
||||
>
|
||||
(how to get one)
|
||||
</a>
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
label=""
|
||||
type="url"
|
||||
value={data.discordWebhookUrl}
|
||||
onChange={(e) => set("discordWebhookUrl", e.target.value)}
|
||||
onChange={(v) => set("discordWebhookUrl", v)}
|
||||
disabled={loading}
|
||||
style={inputStyle}
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "1.25rem" }}>
|
||||
<label style={labelStyle}>
|
||||
<div className="mb-md">
|
||||
<label className="form-label">
|
||||
Slack Webhook URL{" "}
|
||||
<a
|
||||
href="https://api.slack.com/messaging/webhooks"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: "#4499ff", fontSize: "0.8rem" }}
|
||||
className="help-link"
|
||||
>
|
||||
(how to get one)
|
||||
</a>
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
label=""
|
||||
type="url"
|
||||
value={data.slackWebhookUrl}
|
||||
onChange={(e) => set("slackWebhookUrl", e.target.value)}
|
||||
onChange={(v) => set("slackWebhookUrl", v)}
|
||||
disabled={loading}
|
||||
style={inputStyle}
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<label style={labelStyle}>Email address for alerts</label>
|
||||
<input
|
||||
type="email"
|
||||
value={data.notificationEmail}
|
||||
onChange={(e) => set("notificationEmail", e.target.value)}
|
||||
disabled={loading}
|
||||
style={inputStyle}
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Email address for alerts"
|
||||
type="email"
|
||||
value={data.notificationEmail}
|
||||
onChange={(v) => set("notificationEmail", v)}
|
||||
disabled={loading}
|
||||
placeholder="you@example.com"
|
||||
className="form-field--last"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -510,37 +453,27 @@ export default function Onboarding() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 style={{ marginBottom: "1rem" }}>You're all set!</h2>
|
||||
<h2 className="mb-md">You're all set!</h2>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#1e2e1e",
|
||||
border: "1px solid #2d5a2d",
|
||||
borderRadius: "6px",
|
||||
padding: "1rem 1.25rem",
|
||||
marginBottom: "1.5rem",
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: "0 0 0.5rem", color: "#90ee90", fontWeight: "bold" }}>
|
||||
Summary
|
||||
</p>
|
||||
<ul style={{ margin: 0, paddingLeft: "1.25rem", color: "#ccc", lineHeight: "1.8" }}>
|
||||
<div className="onboarding__summary">
|
||||
<p className="onboarding__summary-title">Summary</p>
|
||||
<ul className="onboarding__summary-list">
|
||||
<li>
|
||||
Wallet address added:{" "}
|
||||
<span style={{ color: "white", fontFamily: "monospace", fontSize: "0.85rem" }}>
|
||||
<span className="text-mono text-white-sm">
|
||||
{data.walletAddress}
|
||||
</span>
|
||||
{data.walletLabel && ` (${data.walletLabel})`}
|
||||
</li>
|
||||
<li>
|
||||
Alert rules configured:{" "}
|
||||
<span style={{ color: "white" }}>
|
||||
<span className="text-white">
|
||||
{alertCount > 0 ? `${alertCount} rule${alertCount !== 1 ? "s" : ""}` : "None (skipped)"}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
Notification channels:{" "}
|
||||
<span style={{ color: "white" }}>
|
||||
<span className="text-white">
|
||||
{hasNotif ? "Configured" : "Not set up (skipped)"}
|
||||
</span>
|
||||
</li>
|
||||
@@ -548,39 +481,27 @@ export default function Onboarding() {
|
||||
</div>
|
||||
|
||||
{hasNotif && (
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<button
|
||||
<div className="mb-lg">
|
||||
<Button
|
||||
onClick={handleTestChannels}
|
||||
disabled={testLoading}
|
||||
style={{
|
||||
padding: "0.6rem 1.25rem",
|
||||
backgroundColor: testLoading ? "#333" : "#1a4d80",
|
||||
color: "white",
|
||||
border: "1px solid #0066cc",
|
||||
borderRadius: "4px",
|
||||
cursor: testLoading ? "not-allowed" : "pointer",
|
||||
fontSize: "0.9rem",
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
{testLoading ? "Testing..." : "Test All Channels"}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{testResults && (
|
||||
<div style={{ marginTop: "0.75rem" }}>
|
||||
<div className="mt-md">
|
||||
{testResults.error ? (
|
||||
<p style={{ color: "#ff6666" }}>{testResults.error}</p>
|
||||
<p className="text-error">{testResults.error}</p>
|
||||
) : (
|
||||
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
|
||||
<ul className="list-unstyled">
|
||||
{Object.entries(testResults).map(([channel, result]) => (
|
||||
<li
|
||||
key={channel}
|
||||
style={{
|
||||
color: result.success ? "#90ee90" : "#ff6666",
|
||||
fontSize: "0.9rem",
|
||||
marginBottom: "0.25rem",
|
||||
}}
|
||||
className={`test-result ${result.success ? "test-result--success" : "test-result--failure"}`}
|
||||
>
|
||||
{result.success ? "✓" : "✗"} {channel}:{" "}
|
||||
{result.success ? "\u2713" : "\u2717"} {channel}:{" "}
|
||||
{result.message || (result.success ? "OK" : "Failed")}
|
||||
</li>
|
||||
))}
|
||||
@@ -591,21 +512,9 @@ export default function Onboarding() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => navigate("/addresses")}
|
||||
style={{
|
||||
padding: "0.75rem 2rem",
|
||||
backgroundColor: "#0066cc",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => navigate("/addresses")} className="btn--lg text-bold">
|
||||
Go to Dashboard →
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -614,21 +523,13 @@ export default function Onboarding() {
|
||||
|
||||
function CheckboxRow({ checked, onChange, label, children }) {
|
||||
return (
|
||||
<div style={{ marginBottom: "1rem" }}>
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.6rem",
|
||||
cursor: "pointer",
|
||||
color: "#ddd",
|
||||
}}
|
||||
>
|
||||
<div className="checkbox-row">
|
||||
<label className="checkbox-row__label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
style={{ width: "16px", height: "16px", accentColor: "#0066cc" }}
|
||||
className="checkbox-row__input"
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
@@ -666,67 +567,36 @@ export default function Onboarding() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: "1.5rem",
|
||||
paddingTop: "1rem",
|
||||
borderTop: "1px solid #333",
|
||||
}}
|
||||
>
|
||||
<div className="onboarding__footer">
|
||||
<div>
|
||||
{canBack && (
|
||||
<button
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
backgroundColor: "transparent",
|
||||
color: "#aaa",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
cursor: loading ? "not-allowed" : "pointer",
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: "0.75rem" }}>
|
||||
<div className="flex gap-md">
|
||||
{canSkip && (
|
||||
<button
|
||||
<Button
|
||||
onClick={handleSkip}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: "0.5rem 1rem",
|
||||
backgroundColor: "transparent",
|
||||
color: "#aaa",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "4px",
|
||||
cursor: loading ? "not-allowed" : "pointer",
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
Skip for now
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: "0.5rem 1.25rem",
|
||||
backgroundColor: loading ? "#333" : "#0066cc",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: loading ? "not-allowed" : "pointer",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
className="text-bold"
|
||||
>
|
||||
{loading ? "Please wait..." : step === 4 ? "Finish" : "Next →"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -743,89 +613,29 @@ export default function Onboarding() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
backgroundColor: "#1a1a1a",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
paddingTop: "3rem",
|
||||
paddingBottom: "3rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "100%", maxWidth: "540px", padding: "0 1rem" }}>
|
||||
<h1
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginBottom: "2rem",
|
||||
color: "#0066cc",
|
||||
letterSpacing: "0.5px",
|
||||
}}
|
||||
>
|
||||
Koin Ping
|
||||
</h1>
|
||||
<div className="onboarding">
|
||||
<div className="onboarding__container">
|
||||
<h1 className="onboarding__title">Koin Ping</h1>
|
||||
|
||||
<ProgressBar />
|
||||
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: "0.75rem 1rem",
|
||||
marginBottom: "1rem",
|
||||
backgroundColor: "#3a1a1a",
|
||||
border: "1px solid #cc3333",
|
||||
borderRadius: "4px",
|
||||
color: "#ff6666",
|
||||
fontSize: "0.9rem",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
<div className="alert alert--error">{error}</div>
|
||||
)}
|
||||
|
||||
{skipWarning && (
|
||||
<div
|
||||
style={{
|
||||
padding: "0.75rem 1rem",
|
||||
marginBottom: "1rem",
|
||||
backgroundColor: "#3a2e00",
|
||||
border: "1px solid #aa7700",
|
||||
borderRadius: "4px",
|
||||
color: "#ffcc44",
|
||||
fontSize: "0.9rem",
|
||||
}}
|
||||
>
|
||||
{skipWarning}
|
||||
</div>
|
||||
<div className="alert alert--warning">{skipWarning}</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#242424",
|
||||
border: "1px solid #333",
|
||||
borderRadius: "8px",
|
||||
padding: "2rem",
|
||||
}}
|
||||
>
|
||||
<div className="onboarding__card">
|
||||
{stepContent[step]}
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
{step === 1 && (
|
||||
<p
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: "1.25rem",
|
||||
color: "#888",
|
||||
fontSize: "0.9rem",
|
||||
}}
|
||||
>
|
||||
<p className="onboarding__login-link">
|
||||
Already have an account?{" "}
|
||||
<a href="/login" style={{ color: "#0066cc" }}>
|
||||
Log in here
|
||||
</a>
|
||||
<a href="/login">Log in here</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user