Compare commits

..

6 Commits

Author SHA1 Message Date
KS Jannette
b3a08e8f25 css trick
Some checks are pending
check / check (push) Waiting to run
2026-03-02 00:28:13 -05:00
KS Jannette
8716be0201 fudge 2026-03-02 00:09:24 -05:00
KS Jannette
647be874e5 m 2026-03-02 00:09:01 -05:00
KS Jannette
c12c4fe742 add image 2026-03-01 23:14:25 -05:00
S Jannette
5f5253d495 Revise README for clarity and feature description
Updated the README to clarify features and improve readability.
2026-03-01 21:48:53 -05:00
S Jannette
bbe2374703 Merge pull request #11 from kjannette/history-reconfig-possible-bugfix
History reconfig possible bugfix
2026-03-01 20:38:56 -05:00
5 changed files with 192 additions and 142 deletions

View File

@@ -5,7 +5,6 @@ A lightweight on-chain monitoring and alerting system designed to give users sit
Koin Ping observes on-chain activity and notifies users when predefined conditions are met. It does not execute transactions, manage wallets, or speculate on prices. Koin Ping observes on-chain activity and notifies users when predefined conditions are met. It does not execute transactions, manage wallets, or speculate on prices.
## Getting Started ## Getting Started
### Prerequisites ### Prerequisites
@@ -46,16 +45,16 @@ cd frontend && npm run dev
``` ```
The API listens on `http://localhost:3001` and the frontend on The API listens on `http://localhost:3001` and the frontend on
`http://localhost:3000` by default. `http://localhost:3000` by default for development.
## Rationale ## Rationale
Crypto users who hold or actively monitor addresses need a lightweight, reliable Crypto users may actively monitor addresses with webhooks integrating popular
way to know when on-chain activity occurs without polling block explorers messaging platforms: Discord, Slack, Telegram.
manually. Koin Ping fills that gap: it watches a set of Ethereum addresses, This lightweight, reliable framework makes instant awareness of on-chain
evaluates configurable alert rules (incoming transactions, outgoing activity trivial, without polling block explorers manually. Koin Ping watches
transactions, large transfers, balance thresholds), and notifies the user addresses, evaluates configurable alert rules (incoming transactions, outgoing
through Discord webhooks. transactions, "large" transfers, balance thresholds), and sends notifications.
## Design ## Design
@@ -132,7 +131,7 @@ To receive alerts via Telegram, you need to create a bot and get your chat ID.
4. In the JSON response, find the `"chat"` object — the `"id"` field is your **Chat ID** (a numeric value). 4. In the JSON response, find the `"chat"` object — the `"id"` field is your **Chat ID** (a numeric value).
> **Tip:** If the `"result"` array is empty, make sure you sent a message to your bot first, then refresh the page. > **Tip:** If the `'result"` array is empty, make sure you sent a message to your bot first, then refresh the page.
#### 3. Save in Koin Ping #### 3. Save in Koin Ping
@@ -153,3 +152,5 @@ MIT. See [LICENSE](LICENSE).
## Author ## Author
Steven Jannette Steven Jannette

BIN
frontend/public/ping.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -26,8 +26,8 @@ export default function Navbar() {
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}> <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<span style={{ <span style={{
fontWeight: 700, fontWeight: 700,
fontSize: "1.1rem", fontSize: "1.4rem",
color: "#fff", color: "#e62525",
marginRight: "2rem", marginRight: "2rem",
letterSpacing: "0.5px", letterSpacing: "0.5px",
}}> }}>

View File

@@ -0,0 +1,88 @@
.login-page {
min-height: 100vh;
background-image: url(/ping.png);
background-size: 67%;
background-position: center;
background-repeat: no-repeat;
}
.login-card {
max-width: 400px;
margin: 20px auto;
padding: 2rem;
padding-top: 8rem;
border: 1px solid #333;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.75);
}
.login-heading {
margin-bottom: 2rem;
text-align: center;
}
.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;
}
.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 {
margin-top: 1.5rem;
text-align: center;
}
.login-footer-text {
color: #b3b3b3;
}
.login-signup-link {
color: #0066cc;
text-decoration: none;
}

View File

@@ -1,146 +1,107 @@
/**
* Login Page
*
* Allows existing users to sign in with email and password
*/
import { useState } from "react"; import { useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import "./Login.css";
export default function Login() { export default function Login() {
const [email, setEmail] = useState(""); const [isVisible, setIsVisible] = useState(false);
const [password, setPassword] = useState(""); const [email, setEmail] = useState("");
const [error, setError] = useState(""); const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false); const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const { login } = useAuth(); const { login } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
async function handleSubmit(e) { async function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
// Validation // Validation
if (!email || !password) { if (!email || !password) {
setError("Please fill in all fields"); setError("Please fill in all fields");
return; return;
}
try {
setError("");
setLoading(true);
await login(email, password);
navigate("/addresses"); // Redirect to main app
} catch (err) {
setError("Failed to log in: " + err.message);
} finally {
setLoading(false);
}
} }
return ( try {
<div setError("");
style={{ setLoading(true);
maxWidth: "400px", await login(email, password);
margin: "4rem auto", navigate("/addresses"); // Redirect to main app
padding: "2rem", } catch (err) {
border: "1px solid #333", setError("Failed to log in: " + err.message);
borderRadius: "8px", } finally {
}} setLoading(false);
> }
<h1 style={{ marginBottom: "2rem", textAlign: "center" }}> }
Koin Ping - Login
</h1>
{error && ( return (
<div <div className="login-page">
style={{ <div
padding: "0.75rem", className="login-card"
marginBottom: "1rem", onMouseEnter={() => setIsVisible(true)}
backgroundColor: "#ff000020", >
border: "1px solid #ff0000", <h1 className="login-heading">
borderRadius: "4px", <span className="login-brand">Koin Ping</span> - Login
color: "#ff6666", </h1>
}}
>
{error}
</div>
)}
<form onSubmit={handleSubmit}> <div style={{ visibility: isVisible ? 'visible' : 'hidden' }}>
<div style={{ marginBottom: "1rem" }}> {error && (
<label style={{ display: "block", marginBottom: "0.5rem" }}> <div className="login-error">
Email {error}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
style={{
width: "100%",
padding: "0.5rem",
fontSize: "1rem",
backgroundColor: "#242424",
border: "1px solid #444",
borderRadius: "4px",
color: "white",
}}
required
/>
</div>
<div style={{ marginBottom: "1.5rem" }}>
<label style={{ display: "block", marginBottom: "0.5rem" }}>
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
style={{
width: "100%",
padding: "0.5rem",
fontSize: "1rem",
backgroundColor: "#242424",
border: "1px solid #444",
borderRadius: "4px",
color: "white",
}}
required
/>
</div>
<button
type="submit"
disabled={loading}
style={{
width: "100%",
padding: "0.75rem",
fontSize: "1rem",
backgroundColor: loading ? "#333" : "#0066cc",
color: "white",
border: "none",
borderRadius: "4px",
cursor: loading ? "not-allowed" : "pointer",
}}
>
{loading ? "Logging in..." : "Log In"}
</button>
</form>
<div style={{ marginTop: "1.5rem", textAlign: "center" }}>
<p style={{ color: "#b3b3b3" }}>
Don't have an account?{" "}
<Link
to="/signup"
style={{ color: "#0066cc", textDecoration: "none" }}
>
Sign up here
</Link>
</p>
</div> </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"
disabled={loading}
className="login-button"
>
{loading ? "Logging in..." : "Log In"}
</button>
</form>
<div className="login-footer">
<p className="login-footer-text">
Don't have an account?{" "}
<Link
to="/signup"
className="login-signup-link"
>
Sign up here
</Link>
</p>
</div>
</div> </div>
); </div>
</div>
);
} }