Compare commits

...

4 Commits

Author SHA1 Message Date
KS Jannette
63eecaaf72 m
Some checks are pending
check / check (push) Waiting to run
2026-03-02 03:42:58 -05:00
KS Jannette
8a5f5db903 refact css sytem 2026-03-02 03:36:21 -05:00
KS Jannette
c2515472b2 more css hottie 2026-03-02 00:34:34 -05:00
S Jannette
5ad51271af Merge pull request #12 from kjannette/migrate-inline-css
Migrate inline css
2026-03-02 00:28:46 -05:00
20 changed files with 1093 additions and 1045 deletions

View File

@@ -37,7 +37,7 @@ export default function AddressForm({ onSubmit }) {
placeholder="Treasury, Cold Wallet, etc." placeholder="Treasury, Cold Wallet, etc."
/> />
<Button type="submit" disabled={!canSubmit}> <Button type="submit" disabled={!canSubmit} className="mt-md">
Add Address Add Address
</Button> </Button>
</form> </form>

View File

@@ -0,0 +1,7 @@
.alert-form__options {
margin-bottom: 1rem;
}
.alert-form__radio {
display: block;
}

View File

@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import Input from "./Input"; import Input from "./Input";
import Button from "./Button"; import Button from "./Button";
import "./AlertForm.css";
const ALERT_TYPES = [ const ALERT_TYPES = [
{ value: "incoming_tx", label: "Incoming transaction" }, { value: "incoming_tx", label: "Incoming transaction" },
@@ -36,9 +37,9 @@ export default function AlertForm({ onSubmit }) {
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div style={{ marginBottom: "1rem" }}> <div className="alert-form__options">
{ALERT_TYPES.map((opt) => ( {ALERT_TYPES.map((opt) => (
<label key={opt.value} style={{ display: "block" }}> <label key={opt.value} className="alert-form__radio">
<input <input
type="radio" type="radio"
name="alertType" name="alertType"

View 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;
}

View File

@@ -1,20 +1,19 @@
import "./Button.css";
export default function Button({ export default function Button({
children, children,
onClick, onClick,
disabled = false, disabled = false,
type = "button", type = "button",
variant = "primary",
className = "",
}) { }) {
return ( return (
<button <button
type={type} type={type}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
style={{ className={`btn btn--${variant} ${className}`}
padding: "0.5rem 1rem",
fontSize: "1rem",
cursor: disabled ? "not-allowed" : "pointer",
opacity: disabled ? 0.6 : 1,
}}
> >
{children} {children}
</button> </button>

View File

@@ -0,0 +1,21 @@
.input__label {
display: block;
margin-bottom: 0.4rem;
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);
}

View File

@@ -1,3 +1,5 @@
import "./Input.css";
export default function Input({ export default function Input({
label, label,
type = "text", type = "text",
@@ -7,12 +9,12 @@ export default function Input({
step, step,
min, min,
disabled = false, disabled = false,
required = false,
className = "",
}) { }) {
return ( return (
<label style={{ display: "block", marginBottom: "1rem" }}> <label className={`form-field ${className}`}>
<div style={{ marginBottom: "0.25rem", fontSize: "0.9rem" }}> <div className="input__label">{label}</div>
{label}
</div>
<input <input
type={type} type={type}
value={value} value={value}
@@ -20,12 +22,9 @@ export default function Input({
step={step} step={step}
min={min} min={min}
disabled={disabled} disabled={disabled}
required={required}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
style={{ className="input__control"
width: "100%",
padding: "0.5rem",
fontSize: "1rem",
}}
/> />
</label> </label>
); );

View 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;
}

View File

@@ -1,5 +1,6 @@
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import "./Navbar.css";
const navLinks = [ const navLinks = [
{ to: "/addresses", label: "Addresses" }, { to: "/addresses", label: "Addresses" },
@@ -14,43 +15,17 @@ export default function Navbar() {
if (!currentUser) return null; if (!currentUser) return null;
return ( return (
<nav style={{ <nav className="navbar">
display: "flex", <div className="flex flex--center gap-sm">
justifyContent: "space-between", <span className="navbar__brand">Koin Ping</span>
alignItems: "center", <div className="navbar__links">
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" }}>
{navLinks.map(({ to, label }) => { {navLinks.map(({ to, label }) => {
const isActive = location.pathname === to; const isActive = location.pathname === to;
return ( return (
<Link <Link
key={to} key={to}
to={to} to={to}
style={{ className={`navbar__link ${isActive ? "navbar__link--active" : ""}`}
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",
}}
> >
{label} {label}
</Link> </Link>
@@ -59,24 +34,9 @@ export default function Navbar() {
</div> </div>
</div> </div>
<div style={{ display: "flex", alignItems: "center", gap: "1.25rem" }}> <div className="flex flex--center gap-lg">
<span style={{ fontSize: "1.275rem", color: "#777" }}> <span className="navbar__user">{currentUser.email}</span>
{currentUser.email} <button onClick={logout} className="navbar__logout">
</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",
}}
>
Logout Logout
</button> </button>
</div> </div>

View File

@@ -1,5 +1,43 @@
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap'); @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, *::before,
*::after { *::after {
@@ -21,13 +59,300 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
h1, h2, h3, h4, h5, h6 { h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Nunito', sans-serif; font-family: 'Nunito', sans-serif;
font-weight: 200; font-weight: 200;
} }
input, select, textarea, button { input,
select,
textarea,
button {
font-family: 'Nunito', sans-serif; font-family: 'Nunito', sans-serif;
font-size: 1rem; font-size: 1rem;
font-weight: 200; 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: 1.25rem;
}
.form-field--last {
margin-bottom: 1.75rem;
}
.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;
}

View 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;
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import AddressForm from "../components/AddressForm"; import AddressForm from "../components/AddressForm";
import { getAddresses, createAddress, deleteAddress, updateAddress } from "../api/addresses"; import { getAddresses, createAddress, deleteAddress, updateAddress } from "../api/addresses";
import "./Addresses.css";
export default function Addresses() { export default function Addresses() {
const [addresses, setAddresses] = useState([]); const [addresses, setAddresses] = useState([]);
@@ -9,7 +10,6 @@ export default function Addresses() {
const [editingId, setEditingId] = useState(null); const [editingId, setEditingId] = useState(null);
const [editLabel, setEditLabel] = useState(""); const [editLabel, setEditLabel] = useState("");
// Load addresses on mount
useEffect(() => { useEffect(() => {
async function fetchAddresses() { async function fetchAddresses() {
try { try {
@@ -27,7 +27,6 @@ export default function Addresses() {
fetchAddresses(); fetchAddresses();
}, []); }, []);
// Handle new address submission
async function handleAddressSubmit(data) { async function handleAddressSubmit(data) {
try { try {
const newAddress = await createAddress(data); const newAddress = await createAddress(data);
@@ -78,51 +77,35 @@ export default function Addresses() {
} }
return ( return (
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}> <div className="page">
<h1>Tracked Addresses</h1> <h1>Add Addresses to Track</h1>
<div style={{ marginBottom: "2rem" }}> <div className="mb-xl">
<AddressForm onSubmit={handleAddressSubmit} /> <AddressForm onSubmit={handleAddressSubmit} />
</div> </div>
<div> <div>
<h2>Existing Addresses</h2> <h2>Existing Tracked Addresses</h2>
{loading && <p>Loading addresses...</p>} {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 && ( {!loading && !error && addresses.length === 0 && (
<p style={{ color: "#808080" }}> <p className="text-dimmed">
No addresses tracked yet. Add one above to get started. No addresses tracked yet. Add one above to get started.
</p> </p>
)} )}
{addresses.length > 0 && ( {addresses.length > 0 && (
<ul style={{ listStyle: "none", padding: 0 }}> <ul className="list-unstyled">
{addresses.map((addr, index) => ( {addresses.map((addr, index) => (
<li <li key={addr.id || index} className="list-item--card">
key={addr.id || index} <div className="flex flex--between flex--center">
style={{ <div>
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 }}>
{editingId === addr.id ? ( {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 <input
value={editLabel} value={editLabel}
onChange={(e) => setEditLabel(e.target.value)} onChange={(e) => setEditLabel(e.target.value)}
placeholder="Label (optional)" placeholder="Label (optional)"
style={{ className="address__edit-input"
background: "#444",
border: "1px solid #666",
borderRadius: "3px",
color: "#fff",
padding: "0.25rem 0.5rem",
fontSize: "0.9rem",
}}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleEditSave(addr.id); if (e.key === "Enter") handleEditSave(addr.id);
if (e.key === "Escape") handleEditCancel(); if (e.key === "Escape") handleEditCancel();
@@ -131,61 +114,37 @@ export default function Addresses() {
/> />
<button <button
onClick={() => handleEditSave(addr.id)} onClick={() => handleEditSave(addr.id)}
style={{ cursor: "pointer", padding: "0.25rem 0.6rem", fontSize: "0.85rem" }} className="btn btn--primary btn--sm"
> >
Save Save
</button> </button>
<button <button
onClick={handleEditCancel} 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 Cancel
</button> </button>
</div> </div>
) : ( ) : (
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.25rem" }}> <div className="flex flex--center gap-sm mb-sm">
<span style={{ fontWeight: "bold" }}> <span className="text-bold">
{addr.label || "Unlabeled"} {addr.label || "Unlabeled"}
</span> </span>
<button <button
onClick={() => handleEditStart(addr)} onClick={() => handleEditStart(addr)}
style={{ className="address__edit-link"
cursor: "pointer",
background: "transparent",
border: "none",
color: "#6699cc",
fontSize: "0.8rem",
padding: "0",
textDecoration: "underline",
}}
> >
Edit Edit
</button> </button>
</div> </div>
)} )}
<div <div className="text-mono text-sm text-muted">
style={{
fontFamily: "monospace",
fontSize: "1.035rem",
color: "#b3b3b3",
}}
>
{addr.address} {addr.address}
</div> </div>
</div> </div>
<button <button
onClick={() => handleDelete(addr.id, addr.label)} onClick={() => handleDelete(addr.id, addr.label)}
style={{ className="address__remove"
cursor: "pointer",
background: "transparent",
border: "1px solid #884444",
color: "#cc6666",
borderRadius: "3px",
padding: "0.3rem 0.7rem",
fontSize: "0.85rem",
marginLeft: "1rem",
flexShrink: 0,
}}
> >
Remove Remove
</button> </button>

View 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);
}

View File

@@ -1,12 +1,12 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { getAlertEvents } from "../api/alertEvents"; import { getAlertEvents } from "../api/alertEvents";
import "./AlertHistory.css";
export default function AlertHistory() { export default function AlertHistory() {
const [alertEvents, setAlertEvents] = useState([]); const [alertEvents, setAlertEvents] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
// Fetch alert events on mount
useEffect(() => { useEffect(() => {
async function fetchAlertEvents() { async function fetchAlertEvents() {
try { try {
@@ -25,49 +25,32 @@ export default function AlertHistory() {
}, []); }, []);
if (loading) { if (loading) {
return <div style={{ padding: "2rem" }}>Loading...</div>; return <div className="page">Loading...</div>;
} }
if (error) { if (error) {
return ( return (
<div style={{ padding: "2rem", color: "red" }}>Error: {error}</div> <div className="page text-error">Error: {error}</div>
); );
} }
return ( return (
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}> <div className="page">
<h1>Recent Alert Events</h1> <h1>Recent Alert Events</h1>
{alertEvents.length === 0 ? ( {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) => ( {alertEvents.map((event) => (
<li <li key={event.id} className="alert-history__item">
key={event.id} <div className="mb-sm">{event.message}</div>
style={{
padding: "1rem",
marginBottom: "0.75rem",
border: "1px solid #444",
borderRadius: "4px",
backgroundColor: "#333",
}}
>
<div style={{ marginBottom: "0.5rem" }}>
{event.message}
</div>
{event.address_label && ( {event.address_label && (
<div <div className="text-sm text-dimmed">
style={{
fontSize: "1.035rem",
color: "#808080",
marginBottom: "0.25rem",
}}
>
Address: {event.address_label} Address: {event.address_label}
</div> </div>
)} )}
<small style={{ color: "#b3b3b3" }}> <small className="text-muted">
{formatTimestamp(event.timestamp)} {formatTimestamp(event.timestamp)}
</small> </small>
</li> </li>
@@ -78,7 +61,6 @@ export default function AlertHistory() {
); );
} }
// Helper to format timestamp for display
function formatTimestamp(timestamp) { function formatTimestamp(timestamp) {
const date = new Date(timestamp); const date = new Date(timestamp);
return date.toLocaleString(); return date.toLocaleString();

View 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;
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import AlertForm from "../components/AlertForm"; import AlertForm from "../components/AlertForm";
import Button from "../components/Button"; import Button from "../components/Button";
import Input from "../components/Input";
import { getAddresses } from "../api/addresses"; import { getAddresses } from "../api/addresses";
import { import {
getAlerts, getAlerts,
@@ -15,35 +16,7 @@ import {
setupEmail, setupEmail,
sendEmailDigest, sendEmailDigest,
} from "../api/notificationConfig"; } from "../api/notificationConfig";
import "./Alerts.css";
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",
};
export default function Alerts() { export default function Alerts() {
const [addresses, setAddresses] = useState([]); const [addresses, setAddresses] = useState([]);
@@ -52,7 +25,6 @@ export default function Alerts() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
// Notification config state
const [notificationEnabled, setNotificationEnabled] = useState(false); const [notificationEnabled, setNotificationEnabled] = useState(false);
const [discordWebhookUrl, setDiscordWebhookUrl] = useState(""); const [discordWebhookUrl, setDiscordWebhookUrl] = useState("");
const [telegramBotToken, setTelegramBotToken] = useState(""); const [telegramBotToken, setTelegramBotToken] = useState("");
@@ -273,42 +245,28 @@ export default function Alerts() {
const selectedAddress = addresses.find((a) => a.id === selectedAddressId); const selectedAddress = addresses.find((a) => a.id === selectedAddressId);
if (loading) { if (loading) {
return <div style={{ padding: "2rem" }}>Loading addresses...</div>; return <div className="page">Loading addresses...</div>;
} }
if (addresses.length === 0) { if (addresses.length === 0) {
return ( return (
<div style={{ padding: "2rem" }}> <div className="page">
<p> <p>No addresses tracked yet. Add an address first to create alerts.</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 className="page page--wide">
<h1 style={{ marginBottom: "2rem" }}> <h1 className="mb-xl">Alert Rules & Notifications</h1>
Alert Rules & Notifications
</h1>
<div <div className="alerts-grid">
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "2rem",
alignItems: "start",
}}
>
{/* LEFT COLUMN: Alert Rules */} {/* LEFT COLUMN: Alert Rules */}
<div> <div>
<h2 style={{ marginTop: 0 }}>Alert Rules</h2> <h2 className="mt-0">Alert Rules</h2>
<div style={{ marginBottom: "2rem" }}> <div className="mb-xl">
<label <label className="form-label mb-sm">
style={{ display: "block", marginBottom: "0.5rem" }}
>
<strong>Select Address:</strong> <strong>Select Address:</strong>
</label> </label>
<select <select
@@ -316,15 +274,7 @@ export default function Alerts() {
onChange={(e) => onChange={(e) =>
setSelectedAddressId(Number(e.target.value)) setSelectedAddressId(Number(e.target.value))
} }
style={{ className="form-select"
width: "100%",
padding: "0.5rem",
fontSize: "1rem",
backgroundColor: "#1a1a1a",
border: "1px solid #444",
borderRadius: "4px",
color: "white",
}}
> >
{addresses.map((addr) => ( {addresses.map((addr) => (
<option key={addr.id} value={addr.id}> <option key={addr.id} value={addr.id}>
@@ -336,43 +286,19 @@ export default function Alerts() {
{selectedAddress && ( {selectedAddress && (
<> <>
<div <div className="alerts__address-info">
style={{ <div className="text-sm text-muted">
padding: "1rem",
marginBottom: "2rem",
backgroundColor: "#333",
borderRadius: "4px",
border: "1px solid #444",
}}
>
<div
style={{
fontSize: "1.035rem",
color: "#b3b3b3",
}}
>
Managing alerts for: Managing alerts for:
</div> </div>
<div <div className="text-bold">
style={{
fontWeight: "bold",
marginTop: "0.25rem",
}}
>
{selectedAddress.label || "Unlabeled"} {selectedAddress.label || "Unlabeled"}
</div> </div>
<div <div className="text-mono text-sm text-muted">
style={{
fontFamily: "monospace",
fontSize: "1.035rem",
color: "#b3b3b3",
}}
>
{selectedAddress.address} {selectedAddress.address}
</div> </div>
</div> </div>
<div style={{ marginBottom: "2rem" }}> <div className="mb-xl">
<h3>Create New Alert</h3> <h3>Create New Alert</h3>
<AlertForm onSubmit={handleAlertSubmit} /> <AlertForm onSubmit={handleAlertSubmit} />
</div> </div>
@@ -380,112 +306,43 @@ export default function Alerts() {
<div> <div>
<h3>Active Alert Rules</h3> <h3>Active Alert Rules</h3>
{error && ( {error && (
<p style={{ color: "red" }}> <p className="text-error">Error: {error}</p>
Error: {error}
</p>
)} )}
{alerts.length === 0 ? ( {alerts.length === 0 ? (
<p style={{ color: "#666" }}> <p className="text-dimmed">
No alert rules defined yet. Create one No alert rules defined yet. Create one above.
above.
</p> </p>
) : ( ) : (
<ul <ul className="list-unstyled">
style={{
listStyle: "none",
padding: 0,
}}
>
{alerts.map((alert) => ( {alerts.map((alert) => (
<li <li
key={alert.id} key={alert.id}
style={{ className={`alerts__rule ${!alert.enabled ? "alerts__rule--disabled" : ""}`}
padding: "1rem",
marginBottom: "0.5rem",
border: "1px solid #444",
borderRadius: "4px",
backgroundColor: "#333",
opacity: alert.enabled
? 1
: 0.6,
}}
> >
<div <div className="flex flex--between">
style={{ <div>
display: "flex", <div className="text-bold mb-sm">
justifyContent: {formatAlertType(alert.type)}
"space-between",
alignItems:
"flex-start",
}}
>
<div style={{ flex: 1 }}>
<div
style={{
fontWeight:
"bold",
marginBottom:
"0.25rem",
}}
>
{formatAlertType(
alert.type,
)}
</div> </div>
{alert.threshold && ( {alert.threshold && (
<div <div className="text-sm text-muted">
style={{ Threshold: {alert.threshold} ETH
fontSize:
"1.035rem",
color: "#b3b3b3",
}}
>
Threshold:{" "}
{
alert.threshold
}{" "}
ETH
</div> </div>
)} )}
<div <div className="text-xs text-dimmed">
style={{ Status: {alert.enabled ? "Enabled" : "Disabled"}
fontSize:
"0.978rem",
color: "#808080",
marginTop:
"0.25rem",
}}
>
Status:{" "}
{alert.enabled
? "Enabled"
: "Disabled"}
</div> </div>
</div> </div>
<div <div className="flex flex--center gap-sm">
style={{
display: "flex",
gap: "0.5rem",
}}
>
<Button <Button
onClick={() => onClick={() =>
handleToggleAlert( handleToggleAlert(alert.id, alert.enabled)
alert.id,
alert.enabled,
)
} }
> >
{alert.enabled {alert.enabled ? "Disable" : "Enable"}
? "Disable"
: "Enable"}
</Button> </Button>
<Button <Button
onClick={() => onClick={() => handleDeleteAlert(alert.id)}
handleDeleteAlert(
alert.id,
)
}
> >
Delete Delete
</Button> </Button>
@@ -502,329 +359,152 @@ export default function Alerts() {
{/* RIGHT COLUMN: Notification Settings */} {/* RIGHT COLUMN: Notification Settings */}
<div> <div>
<h2 style={{ marginTop: 0 }}>Notification Settings</h2> <h2 className="mt-0">Notification Settings</h2>
{notificationSuccess && ( {notificationSuccess && (
<div <div className="alert alert--success">{notificationSuccess}</div>
style={{
padding: "0.75rem",
marginBottom: "1rem",
backgroundColor: "#00ff0020",
border: "1px solid #00ff00",
borderRadius: "4px",
color: "#00ff00",
}}
>
{notificationSuccess}
</div>
)} )}
{notificationError && ( {notificationError && (
<div <div className="alert alert--error">{notificationError}</div>
style={{
padding: "0.75rem",
marginBottom: "1rem",
backgroundColor: "#ff000020",
border: "1px solid #ff0000",
borderRadius: "4px",
color: "#ff6666",
}}
>
{notificationError}
</div>
)} )}
{/* Master toggle */} {/* Master toggle */}
<div <div className="alerts__toggle-panel">
style={{ <label className="alerts__toggle-label">
marginBottom: "1.5rem",
padding: "1rem",
backgroundColor: "#333",
borderRadius: "4px",
}}
>
<label
style={{
display: "flex",
alignItems: "center",
cursor: "pointer",
}}
>
<input <input
type="checkbox" type="checkbox"
checked={notificationEnabled} checked={notificationEnabled}
onChange={(e) => onChange={(e) =>
setNotificationEnabled(e.target.checked) setNotificationEnabled(e.target.checked)
} }
style={{ className="alerts__toggle-checkbox"
marginRight: "0.5rem",
width: "18px",
height: "18px",
}}
/> />
<span style={{ fontWeight: "bold" }}> <span className="text-bold">Enable Notifications</span>
Enable Notifications
</span>
</label> </label>
<div <div className="alerts__toggle-hint">
style={{
fontSize: "0.978rem",
color: "#808080",
marginTop: "0.5rem",
marginLeft: "26px",
}}
>
Master switch for all notification channels Master switch for all notification channels
</div> </div>
</div> </div>
{/* All channel settings hidden when master toggle is off */} {/* All channel settings -- hidden when master toggle is off */}
<div {notificationEnabled && (
style={{ <>
display: notificationEnabled ? "block" : "none", {/* Telegram */}
}} <div className="section">
> <h3 className="mt-0 mb-md">Telegram</h3>
{/* Telegram */} <Input
<div style={sectionStyle}> label="Bot Token"
<h3 style={{ marginTop: 0, marginBottom: "1rem" }}>
Telegram
</h3>
<div style={{ marginBottom: "0.75rem" }}>
<label style={labelStyle}>Bot Token</label>
<input
type="text"
value={telegramBotToken} value={telegramBotToken}
onChange={(e) => onChange={setTelegramBotToken}
setTelegramBotToken(e.target.value)
}
placeholder="123456789:ABCdefGHIjklMNOpqrSTUvwxYZ" placeholder="123456789:ABCdefGHIjklMNOpqrSTUvwxYZ"
style={inputStyle}
/> />
</div> <Input
label="Chat ID"
<div style={{ marginBottom: "0.5rem" }}>
<label style={labelStyle}>Chat ID</label>
<input
type="text"
value={telegramChatId} value={telegramChatId}
onChange={(e) => onChange={setTelegramChatId}
setTelegramChatId(e.target.value)
}
placeholder="-1001234567890" 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> </div>
<a {/* Email */}
href="https://core.telegram.org/bots#how-do-i-create-a-bot" <div className="section">
target="_blank" <h3 className="mt-0 mb-md">Email</h3>
rel="noopener noreferrer" <Input
style={helpLinkStyle} label="Email Address"
>
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
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={setEmail}
placeholder="you@example.com" 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>
<div {/* Discord */}
style={{ <div className="section">
fontSize: "0.85rem", <h3 className="mt-0 mb-md">Discord</h3>
color: "#808080", <Input
marginBottom: "0.75rem", label="Discord Webhook URL"
}}
>
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"
value={discordWebhookUrl} value={discordWebhookUrl}
onChange={(e) => onChange={setDiscordWebhookUrl}
setDiscordWebhookUrl(e.target.value)
}
placeholder="https://discord.com/api/webhooks/..." 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> </div>
<a {/* Slack */}
href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" <div className="section">
target="_blank" <h3 className="mt-0 mb-md">Slack</h3>
rel="noopener noreferrer" <Input
style={helpLinkStyle} label="Slack Webhook URL"
>
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"
value={slackWebhookUrl} value={slackWebhookUrl}
onChange={(e) => onChange={setSlackWebhookUrl}
setSlackWebhookUrl(e.target.value)
}
placeholder="https://hooks.slack.com/services/..." 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> </div>
<a {/* Save & Test buttons */}
href="https://api.slack.com/messaging/webhooks" <div className="alerts__save-test">
target="_blank" <Button
rel="noopener noreferrer" onClick={handleSaveNotificationConfig}
style={helpLinkStyle} disabled={notificationLoading}
> className="btn--lg"
How to set up Slack Incoming Webhooks >
</a> {notificationLoading ? "Saving..." : "Save Settings"}
</div> </Button>
<Button
{/* Save & Test buttons at the bottom */} onClick={handleTestChannels}
<div disabled={testingChannels}
style={{ variant="success"
display: "flex", className="btn--lg"
gap: "0.75rem", >
marginTop: "1rem", {testingChannels ? "Testing..." : "Test All Channels"}
}} </Button>
> </div>
<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>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -11,8 +11,8 @@
margin: 20px auto; margin: 20px auto;
padding: 2rem; padding: 2rem;
padding-top: 8rem; padding-top: 8rem;
border: 1px solid #333; border: 1px solid var(--color-border-light);
border-radius: 8px; border-radius: var(--radius-xl);
background-color: rgba(0, 0, 0, 0.75); background-color: rgba(0, 0, 0, 0.75);
} }
@@ -22,55 +22,15 @@
} }
.login-brand { .login-brand {
color: #e62525; color: var(--color-brand);
}
.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;
} }
.login-button { .login-button {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
font-size: 1.2rem; font-size: 1.2rem;
background-color: #0066cc; margin-top: 16px;
color: white; ;
border: none;
border-radius: 4px;
cursor: pointer;
}
.login-button:disabled {
background-color: #333;
cursor: not-allowed;
} }
.login-footer { .login-footer {
@@ -78,11 +38,25 @@
text-align: center; text-align: center;
} }
.login-footer-text {
color: #b3b3b3;
}
.login-signup-link { .login-signup-link {
color: #0066cc; color: var(--color-primary);
text-decoration: none; text-decoration: none;
} }
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.login-form-hidden {
opacity: 0;
}
.login-form-visible {
animation: fadeIn 2s ease-in-out forwards;
}

View File

@@ -1,6 +1,7 @@
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";
import Input from "../components/Input";
import "./Login.css"; import "./Login.css";
export default function Login() { export default function Login() {
@@ -16,7 +17,6 @@ export default function Login() {
async function handleSubmit(e) { async function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
// Validation
if (!email || !password) { if (!email || !password) {
setError("Please fill in all fields"); setError("Please fill in all fields");
return; return;
@@ -26,7 +26,7 @@ export default function Login() {
setError(""); setError("");
setLoading(true); setLoading(true);
await login(email, password); await login(email, password);
navigate("/addresses"); // Redirect to main app navigate("/addresses");
} catch (err) { } catch (err) {
setError("Failed to log in: " + err.message); setError("Failed to log in: " + err.message);
} finally { } finally {
@@ -36,66 +36,42 @@ export default function Login() {
return ( return (
<div className="login-page"> <div className="login-page">
<div <div className="login-card" onMouseEnter={() => setIsVisible(true)}>
className="login-card"
onMouseEnter={() => setIsVisible(true)}
>
<h1 className="login-heading"> <h1 className="login-heading">
<span className="login-brand">Koin Ping</span> - Login <span className="login-brand">Koin Ping</span> - Login
</h1> </h1>
<div style={{ visibility: isVisible ? 'visible' : 'hidden' }}> <div className={isVisible ? "login-form-visible" : "login-form-hidden"}>
{error && ( {error && <div className="alert alert--error">{error}</div>}
<div className="login-error">
{error}
</div>
)}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="login-field"> <Input
<label className="login-label"> label="Email"
Email type="email"
</label> value={email}
<input onChange={setEmail}
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"
disabled={loading} 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"} {loading ? "Logging in..." : "Log In"}
</button> </button>
</form> </form>
<div className="login-footer"> <div className="login-footer">
<p className="login-footer-text"> <p className="text-muted">
Don't have an account?{" "} Don't have an account?{" "}
<Link <Link to="/signup" className="login-signup-link">
to="/signup"
className="login-signup-link"
>
Sign up here Sign up here
</Link> </Link>
</p> </p>

View 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;
}

View File

@@ -1,7 +1,7 @@
/** /**
* Onboarding Wizard * 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"; import { useState, useEffect } from "react";
@@ -13,6 +13,9 @@ import {
updateNotificationConfig, updateNotificationConfig,
testNotificationChannels, testNotificationChannels,
} from "../api/notificationConfig"; } from "../api/notificationConfig";
import Input from "../components/Input";
import Button from "../components/Button";
import "./Onboarding.css";
const STEPS = [ const STEPS = [
"Create Account", "Create Account",
@@ -22,24 +25,6 @@ const STEPS = [
"Done", "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() { export default function Onboarding() {
const { currentUser, signup } = useAuth(); const { currentUser, signup } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -51,7 +36,6 @@ export default function Onboarding() {
const [testResults, setTestResults] = useState(null); const [testResults, setTestResults] = useState(null);
const [testLoading, setTestLoading] = useState(false); const [testLoading, setTestLoading] = useState(false);
// Wizard state
const [data, setData] = useState({ const [data, setData] = useState({
email: "", email: "",
password: "", password: "",
@@ -68,7 +52,6 @@ export default function Onboarding() {
discordWebhookUrl: "", discordWebhookUrl: "",
slackWebhookUrl: "", slackWebhookUrl: "",
notificationEmail: "", notificationEmail: "",
// summary
alertsCreated: [], alertsCreated: [],
notificationConfigured: false, notificationConfigured: false,
}); });
@@ -77,7 +60,6 @@ export default function Onboarding() {
setData((prev) => ({ ...prev, [field]: value })); setData((prev) => ({ ...prev, [field]: value }));
} }
// On mount: if already fully onboarded, redirect away
useEffect(() => { useEffect(() => {
if (!currentUser) return; if (!currentUser) return;
getAddresses() getAddresses()
@@ -86,7 +68,7 @@ export default function Onboarding() {
navigate("/addresses", { replace: true }); navigate("/addresses", { replace: true });
} }
}) })
.catch(() => {}); // ignore errors (e.g. mid-signup) .catch(() => {});
}, [currentUser, navigate]); }, [currentUser, navigate]);
// ── Step handlers ───────────────────────────────────────────────────────── // ── Step handlers ─────────────────────────────────────────────────────────
@@ -105,7 +87,6 @@ export default function Onboarding() {
setError("Password must be at least 6 characters"); setError("Password must be at least 6 characters");
return; return;
} }
// If user already exists (browser-close-mid-wizard), skip signup
if (!currentUser) { if (!currentUser) {
try { try {
setLoading(true); setLoading(true);
@@ -237,59 +218,38 @@ export default function Onboarding() {
function ProgressBar() { function ProgressBar() {
return ( return (
<div <div className="progress-bar">
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: "2rem",
}}
>
{STEPS.map((label, i) => { {STEPS.map((label, i) => {
const stepNum = i + 1; const stepNum = i + 1;
const done = step > stepNum; const done = step > stepNum;
const active = step === stepNum; const active = step === stepNum;
const dotClass = done
? "progress-bar__dot--done"
: active
? "progress-bar__dot--active"
: "progress-bar__dot--pending";
return ( return (
<div <div key={label} className="progress-bar__step">
key={label}
style={{ display: "flex", alignItems: "center" }}
>
{i > 0 && ( {i > 0 && (
<div <div
style={{ className={`progress-bar__connector ${
width: "40px", done || active
height: "2px", ? "progress-bar__connector--active"
backgroundColor: done || active ? "#0066cc" : "#444", : "progress-bar__connector--inactive"
margin: "0 4px", }`}
}}
/> />
)} )}
<div style={{ textAlign: "center" }}> <div>
<div <div className={`progress-bar__dot ${dotClass}`}>
style={{ {done ? "\u2713" : stepNum}
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>
<div <div
style={{ className={`progress-bar__label ${
fontSize: "0.7rem", active
color: active ? "white" : "#888", ? "progress-bar__label--active"
whiteSpace: "nowrap", : "progress-bar__label--inactive"
}} }`}
> >
{label} {label}
</div> </div>
@@ -306,40 +266,32 @@ export default function Onboarding() {
function Step1() { function Step1() {
return ( return (
<> <>
<h2 style={{ marginBottom: "1.5rem" }}>Create your account</h2> <h2 className="mb-lg">Create your account</h2>
<div style={{ marginBottom: "1rem" }}> <Input
<label style={labelStyle}>Email</label> label="Email"
<input type="email"
type="email" value={data.email}
value={data.email} onChange={(v) => set("email", v)}
onChange={(e) => set("email", e.target.value)} disabled={loading}
disabled={loading} placeholder="you@example.com"
style={inputStyle} />
placeholder="you@example.com" <Input
/> label="Password"
</div> type="password"
<div style={{ marginBottom: "1rem" }}> value={data.password}
<label style={labelStyle}>Password</label> onChange={(v) => set("password", v)}
<input disabled={loading}
type="password" placeholder="At least 6 characters"
value={data.password} />
onChange={(e) => set("password", e.target.value)} <Input
disabled={loading} label="Confirm Password"
style={inputStyle} type="password"
placeholder="At least 6 characters" value={data.confirmPassword}
/> onChange={(v) => set("confirmPassword", v)}
</div> disabled={loading}
<div style={{ marginBottom: "1.5rem" }}> placeholder="Repeat your password"
<label style={labelStyle}>Confirm Password</label> className="form-field--last"
<input />
type="password"
value={data.confirmPassword}
onChange={(e) => set("confirmPassword", e.target.value)}
disabled={loading}
style={inputStyle}
placeholder="Repeat your password"
/>
</div>
</> </>
); );
} }
@@ -347,32 +299,25 @@ export default function Onboarding() {
function Step2() { function Step2() {
return ( return (
<> <>
<h2 style={{ marginBottom: "0.5rem" }}>Add a wallet address</h2> <h2 className="mb-sm">Add a wallet address</h2>
<p style={{ color: "#aaa", marginBottom: "1.5rem", fontSize: "0.9rem" }}> <p className="onboarding__subtitle">
Enter the Ethereum address you want to monitor. Enter the Ethereum address you want to monitor.
</p> </p>
<div style={{ marginBottom: "1rem" }}> <Input
<label style={labelStyle}>ETH Address</label> label="ETH Address"
<input value={data.walletAddress}
type="text" onChange={(v) => set("walletAddress", v)}
value={data.walletAddress} disabled={loading}
onChange={(e) => set("walletAddress", e.target.value)} placeholder="0x..."
disabled={loading} />
style={inputStyle} <Input
placeholder="0x..." label="Label (optional)"
/> value={data.walletLabel}
</div> onChange={(v) => set("walletLabel", v)}
<div style={{ marginBottom: "1.5rem" }}> disabled={loading}
<label style={labelStyle}>Label (optional)</label> placeholder="e.g. My main wallet"
<input className="form-field--last"
type="text" />
value={data.walletLabel}
onChange={(e) => set("walletLabel", e.target.value)}
disabled={loading}
style={inputStyle}
placeholder="e.g. My main wallet"
/>
</div>
</> </>
); );
} }
@@ -380,8 +325,8 @@ export default function Onboarding() {
function Step3() { function Step3() {
return ( return (
<> <>
<h2 style={{ marginBottom: "0.5rem" }}>Configure alert rules</h2> <h2 className="mb-sm">Configure alert rules</h2>
<p style={{ color: "#aaa", marginBottom: "1.5rem", fontSize: "0.9rem" }}> <p className="onboarding__subtitle">
Choose which events trigger notifications. You can change these later. Choose which events trigger notifications. You can change these later.
</p> </p>
@@ -401,12 +346,12 @@ export default function Onboarding() {
label="Large transfer" label="Large transfer"
> >
{data.alertLargeTransfer && ( {data.alertLargeTransfer && (
<div style={{ marginTop: "0.5rem", marginLeft: "1.75rem" }}> <div className="checkbox-row__nested">
<input <Input
type="number" type="number"
label=""
value={data.largeTransferThreshold} value={data.largeTransferThreshold}
onChange={(e) => set("largeTransferThreshold", e.target.value)} onChange={(v) => set("largeTransferThreshold", v)}
style={{ ...inputStyle, width: "160px" }}
placeholder="Threshold (ETH)" placeholder="Threshold (ETH)"
min="0" min="0"
step="0.01" step="0.01"
@@ -420,12 +365,12 @@ export default function Onboarding() {
label="Balance below" label="Balance below"
> >
{data.alertBalanceBelow && ( {data.alertBalanceBelow && (
<div style={{ marginTop: "0.5rem", marginLeft: "1.75rem" }}> <div className="checkbox-row__nested">
<input <Input
type="number" type="number"
label=""
value={data.balanceBelowThreshold} value={data.balanceBelowThreshold}
onChange={(e) => set("balanceBelowThreshold", e.target.value)} onChange={(v) => set("balanceBelowThreshold", v)}
style={{ ...inputStyle, width: "160px" }}
placeholder="Threshold (ETH)" placeholder="Threshold (ETH)"
min="0" min="0"
step="0.01" step="0.01"
@@ -440,66 +385,64 @@ export default function Onboarding() {
function Step4() { function Step4() {
return ( return (
<> <>
<h2 style={{ marginBottom: "0.5rem" }}>Set up notifications</h2> <h2 className="mb-sm">Set up notifications</h2>
<p style={{ color: "#aaa", marginBottom: "1.5rem", fontSize: "0.9rem" }}> <p className="onboarding__subtitle">
Add at least one channel so you receive alerts. All fields are optional. Add at least one channel so you receive alerts. All fields are optional.
</p> </p>
<div style={{ marginBottom: "1.25rem" }}> <div className="mb-md">
<label style={labelStyle}> <label className="form-label">
Discord Webhook URL{" "} Discord Webhook URL{" "}
<a <a
href="https://support.discord.com/hc/en-us/articles/228383668" href="https://support.discord.com/hc/en-us/articles/228383668"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
style={{ color: "#4499ff", fontSize: "0.8rem" }} className="help-link"
> >
(how to get one) (how to get one)
</a> </a>
</label> </label>
<input <Input
label=""
type="url" type="url"
value={data.discordWebhookUrl} value={data.discordWebhookUrl}
onChange={(e) => set("discordWebhookUrl", e.target.value)} onChange={(v) => set("discordWebhookUrl", v)}
disabled={loading} disabled={loading}
style={inputStyle}
placeholder="https://discord.com/api/webhooks/..." placeholder="https://discord.com/api/webhooks/..."
/> />
</div> </div>
<div style={{ marginBottom: "1.25rem" }}> <div className="mb-md">
<label style={labelStyle}> <label className="form-label">
Slack Webhook URL{" "} Slack Webhook URL{" "}
<a <a
href="https://api.slack.com/messaging/webhooks" href="https://api.slack.com/messaging/webhooks"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
style={{ color: "#4499ff", fontSize: "0.8rem" }} className="help-link"
> >
(how to get one) (how to get one)
</a> </a>
</label> </label>
<input <Input
label=""
type="url" type="url"
value={data.slackWebhookUrl} value={data.slackWebhookUrl}
onChange={(e) => set("slackWebhookUrl", e.target.value)} onChange={(v) => set("slackWebhookUrl", v)}
disabled={loading} disabled={loading}
style={inputStyle}
placeholder="https://hooks.slack.com/services/..." placeholder="https://hooks.slack.com/services/..."
/> />
</div> </div>
<div style={{ marginBottom: "1.5rem" }}> <Input
<label style={labelStyle}>Email address for alerts</label> label="Email address for alerts"
<input type="email"
type="email" value={data.notificationEmail}
value={data.notificationEmail} onChange={(v) => set("notificationEmail", v)}
onChange={(e) => set("notificationEmail", e.target.value)} disabled={loading}
disabled={loading} placeholder="you@example.com"
style={inputStyle} className="form-field--last"
placeholder="you@example.com" />
/>
</div>
</> </>
); );
} }
@@ -510,37 +453,27 @@ export default function Onboarding() {
return ( return (
<> <>
<h2 style={{ marginBottom: "1rem" }}>You're all set!</h2> <h2 className="mb-md">You're all set!</h2>
<div <div className="onboarding__summary">
style={{ <p className="onboarding__summary-title">Summary</p>
backgroundColor: "#1e2e1e", <ul className="onboarding__summary-list">
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" }}>
<li> <li>
Wallet address added:{" "} Wallet address added:{" "}
<span style={{ color: "white", fontFamily: "monospace", fontSize: "0.85rem" }}> <span className="text-mono text-white-sm">
{data.walletAddress} {data.walletAddress}
</span> </span>
{data.walletLabel && ` (${data.walletLabel})`} {data.walletLabel && ` (${data.walletLabel})`}
</li> </li>
<li> <li>
Alert rules configured:{" "} Alert rules configured:{" "}
<span style={{ color: "white" }}> <span className="text-white">
{alertCount > 0 ? `${alertCount} rule${alertCount !== 1 ? "s" : ""}` : "None (skipped)"} {alertCount > 0 ? `${alertCount} rule${alertCount !== 1 ? "s" : ""}` : "None (skipped)"}
</span> </span>
</li> </li>
<li> <li>
Notification channels:{" "} Notification channels:{" "}
<span style={{ color: "white" }}> <span className="text-white">
{hasNotif ? "Configured" : "Not set up (skipped)"} {hasNotif ? "Configured" : "Not set up (skipped)"}
</span> </span>
</li> </li>
@@ -548,39 +481,27 @@ export default function Onboarding() {
</div> </div>
{hasNotif && ( {hasNotif && (
<div style={{ marginBottom: "1.5rem" }}> <div className="mb-lg">
<button <Button
onClick={handleTestChannels} onClick={handleTestChannels}
disabled={testLoading} disabled={testLoading}
style={{ variant="ghost"
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",
}}
> >
{testLoading ? "Testing..." : "Test All Channels"} {testLoading ? "Testing..." : "Test All Channels"}
</button> </Button>
{testResults && ( {testResults && (
<div style={{ marginTop: "0.75rem" }}> <div className="mt-md">
{testResults.error ? ( {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]) => ( {Object.entries(testResults).map(([channel, result]) => (
<li <li
key={channel} key={channel}
style={{ className={`test-result ${result.success ? "test-result--success" : "test-result--failure"}`}
color: result.success ? "#90ee90" : "#ff6666",
fontSize: "0.9rem",
marginBottom: "0.25rem",
}}
> >
{result.success ? "" : ""} {channel}:{" "} {result.success ? "\u2713" : "\u2717"} {channel}:{" "}
{result.message || (result.success ? "OK" : "Failed")} {result.message || (result.success ? "OK" : "Failed")}
</li> </li>
))} ))}
@@ -591,21 +512,9 @@ export default function Onboarding() {
</div> </div>
)} )}
<button <Button onClick={() => navigate("/addresses")} className="btn--lg text-bold">
onClick={() => navigate("/addresses")}
style={{
padding: "0.75rem 2rem",
backgroundColor: "#0066cc",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "1rem",
fontWeight: "bold",
}}
>
Go to Dashboard Go to Dashboard
</button> </Button>
</> </>
); );
} }
@@ -614,21 +523,13 @@ export default function Onboarding() {
function CheckboxRow({ checked, onChange, label, children }) { function CheckboxRow({ checked, onChange, label, children }) {
return ( return (
<div style={{ marginBottom: "1rem" }}> <div className="checkbox-row">
<label <label className="checkbox-row__label">
style={{
display: "flex",
alignItems: "center",
gap: "0.6rem",
cursor: "pointer",
color: "#ddd",
}}
>
<input <input
type="checkbox" type="checkbox"
checked={checked} checked={checked}
onChange={(e) => onChange(e.target.checked)} onChange={(e) => onChange(e.target.checked)}
style={{ width: "16px", height: "16px", accentColor: "#0066cc" }} className="checkbox-row__input"
/> />
{label} {label}
</label> </label>
@@ -666,67 +567,36 @@ export default function Onboarding() {
} }
return ( return (
<div <div className="onboarding__footer">
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: "1.5rem",
paddingTop: "1rem",
borderTop: "1px solid #333",
}}
>
<div> <div>
{canBack && ( {canBack && (
<button <Button
onClick={handleBack} onClick={handleBack}
disabled={loading} disabled={loading}
style={{ variant="ghost"
padding: "0.5rem 1rem",
backgroundColor: "transparent",
color: "#aaa",
border: "1px solid #444",
borderRadius: "4px",
cursor: loading ? "not-allowed" : "pointer",
}}
> >
Back Back
</button> </Button>
)} )}
</div> </div>
<div style={{ display: "flex", gap: "0.75rem" }}> <div className="flex gap-md">
{canSkip && ( {canSkip && (
<button <Button
onClick={handleSkip} onClick={handleSkip}
disabled={loading} disabled={loading}
style={{ variant="ghost"
padding: "0.5rem 1rem",
backgroundColor: "transparent",
color: "#aaa",
border: "1px solid #444",
borderRadius: "4px",
cursor: loading ? "not-allowed" : "pointer",
}}
> >
Skip for now Skip for now
</button> </Button>
)} )}
<button <Button
onClick={handleNext} onClick={handleNext}
disabled={loading} disabled={loading}
style={{ className="text-bold"
padding: "0.5rem 1.25rem",
backgroundColor: loading ? "#333" : "#0066cc",
color: "white",
border: "none",
borderRadius: "4px",
cursor: loading ? "not-allowed" : "pointer",
fontWeight: "bold",
}}
> >
{loading ? "Please wait..." : step === 4 ? "Finish" : "Next →"} {loading ? "Please wait..." : step === 4 ? "Finish" : "Next →"}
</button> </Button>
</div> </div>
</div> </div>
); );
@@ -743,89 +613,29 @@ export default function Onboarding() {
}; };
return ( return (
<div <div className="onboarding">
style={{ <div className="onboarding__container">
minHeight: "100vh", <h1 className="onboarding__title">Koin Ping</h1>
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>
<ProgressBar /> <ProgressBar />
{error && ( {error && (
<div <div className="alert alert--error">{error}</div>
style={{
padding: "0.75rem 1rem",
marginBottom: "1rem",
backgroundColor: "#3a1a1a",
border: "1px solid #cc3333",
borderRadius: "4px",
color: "#ff6666",
fontSize: "0.9rem",
}}
>
{error}
</div>
)} )}
{skipWarning && ( {skipWarning && (
<div <div className="alert alert--warning">{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 <div className="onboarding__card">
style={{
backgroundColor: "#242424",
border: "1px solid #333",
borderRadius: "8px",
padding: "2rem",
}}
>
{stepContent[step]} {stepContent[step]}
<Footer /> <Footer />
</div> </div>
{step === 1 && ( {step === 1 && (
<p <p className="onboarding__login-link">
style={{
textAlign: "center",
marginTop: "1.25rem",
color: "#888",
fontSize: "0.9rem",
}}
>
Already have an account?{" "} Already have an account?{" "}
<a href="/login" style={{ color: "#0066cc" }}> <a href="/login">Log in here</a>
Log in here
</a>
</p> </p>
)} )}
</div> </div>