Add standalone /tools page for bookmarkable quantum random tools
Dedicated tools-only page at /tools with dice, password generator, coin flip, and 8 ball - each with its own click-to-copy output box. Clean, lightweight page without API docs/MCP/contact form clutter. Linked from the main page's QTRNG Tools section. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7bf292d64e
commit
b7bff60271
|
|
@ -397,7 +397,7 @@
|
|||
|
||||
<div class="card">
|
||||
<h2>QTRNG Tools</h2>
|
||||
<p style="margin-bottom:1.25rem;color:var(--date-color);">Quantum dice, passwords, coin flips, and a Magic 8 Ball — all powered by quantum camera noise.</p>
|
||||
<p style="margin-bottom:1.25rem;color:var(--date-color);">Quantum dice, passwords, coin flips, and a Magic 8 Ball — all powered by quantum camera noise. <a href="/tools" title="Bookmark this page">Standalone tools page →</a></p>
|
||||
|
||||
<div class="tools-section">
|
||||
<h3>Quantum Dice</h3>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.route("/password", get(get_password))
|
||||
.route("/coin", get(get_coin))
|
||||
.route("/8ball", get(get_eightball))
|
||||
.route("/tools", get(tools_page))
|
||||
.route("/health", get(health))
|
||||
.route("/.well-known/mcp.json", get(mcp_wellknown))
|
||||
.route("/.well-known/skill.md", get(get_skill_md))
|
||||
|
|
@ -90,6 +91,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
|
||||
async fn index() -> Html<&'static str> { Html(INDEX_HTML) }
|
||||
async fn tools_page() -> Html<&'static str> { Html(TOOLS_HTML) }
|
||||
async fn health() -> &'static str { "ok" }
|
||||
|
||||
async fn get_docs() -> Html<String> {
|
||||
|
|
@ -423,4 +425,5 @@ async fn get_stream(Query(params): Query<StreamQuery>) -> Response {
|
|||
|
||||
|
||||
const INDEX_HTML: &str = include_str!("index.html");
|
||||
const TOOLS_HTML: &str = include_str!("tools.html");
|
||||
const SKILL_MD: &str = include_str!("../skill.md");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,326 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Quantum Random Tools - Dice, passwords, coin flips, and Magic 8 Ball powered by camera sensor quantum noise.">
|
||||
<title>Quantum Random Tools - Camera TRNG</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #ffffff; --text-color: #333333; --accent-color: #0056b3;
|
||||
--border-color: #e0e0e0; --hover-color: #003d82; --theme-bg: #f5f5f5;
|
||||
--theme-hover: #e0e0e0; --date-color: #555555;
|
||||
--bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #eaeaea;
|
||||
--text-primary: #333333; --focus-outline-color: #0056b3;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: #1a1a1a; --text-color: #e0e0e0; --accent-color: #5fa9ff;
|
||||
--border-color: #404040; --hover-color: #8ac2ff; --theme-bg: #2d2d2d;
|
||||
--theme-hover: #3d3d3d; --date-color: #a0a0a0;
|
||||
--bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #3d3d3d;
|
||||
--text-primary: #e0e0e0; --focus-outline-color: #5fa9ff;
|
||||
}
|
||||
}
|
||||
html[data-theme='light'] {
|
||||
--bg-color: #ffffff; --text-color: #333333; --accent-color: #0056b3;
|
||||
--border-color: #e0e0e0; --hover-color: #003d82; --theme-bg: #f5f5f5;
|
||||
--theme-hover: #e0e0e0; --date-color: #555555;
|
||||
--bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #eaeaea;
|
||||
--text-primary: #333333; --focus-outline-color: #0056b3;
|
||||
}
|
||||
html[data-theme='dark'] {
|
||||
--bg-color: #1a1a1a; --text-color: #e0e0e0; --accent-color: #5fa9ff;
|
||||
--border-color: #404040; --hover-color: #8ac2ff; --theme-bg: #2d2d2d;
|
||||
--theme-hover: #3d3d3d; --date-color: #a0a0a0;
|
||||
--bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #3d3d3d;
|
||||
--text-primary: #e0e0e0; --focus-outline-color: #5fa9ff;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.6; color: var(--text-color); background-color: var(--bg-color);
|
||||
margin: 0 auto; padding: 20px; max-width: 700px;
|
||||
}
|
||||
a { color: var(--accent-color); text-decoration: underline; }
|
||||
a:hover { color: var(--hover-color); }
|
||||
.theme-switch { position: fixed; top: 20px; right: 20px; z-index: 1000; }
|
||||
.theme-switch button {
|
||||
background: var(--theme-bg); border: 1px solid var(--border-color); font-size: 1.5em;
|
||||
cursor: pointer; padding: 8px 12px; border-radius: 8px; transition: background-color 0.3s;
|
||||
}
|
||||
.theme-switch button:hover { background-color: var(--theme-hover); }
|
||||
h1 {
|
||||
font-size: 1.75em; color: var(--accent-color); border-bottom: 2px solid var(--accent-color);
|
||||
padding-bottom: 0.3em; margin-bottom: 0.25em;
|
||||
}
|
||||
h3 { font-size: 1.1em; color: var(--text-color); margin-top: 0; margin-bottom: 0.75rem; }
|
||||
.subtitle { color: var(--date-color); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
||||
.back-link { font-size: 0.85rem; margin-bottom: 1rem; display: block; }
|
||||
.card {
|
||||
background: var(--bg-secondary); border: 1px solid var(--border-color);
|
||||
border-radius: 8px; padding: 1.5rem; margin-bottom: 1.25rem;
|
||||
}
|
||||
.controls { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: flex-end; }
|
||||
.field { flex: 1; min-width: 120px; }
|
||||
label {
|
||||
display: block; font-size: 0.85rem; color: var(--date-color);
|
||||
margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
}
|
||||
input, select {
|
||||
width: 100%; padding: 0.75rem 1rem; background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color); border-radius: 4px;
|
||||
color: var(--text-color); font-family: monospace; font-size: 0.9rem;
|
||||
}
|
||||
input:focus, select:focus {
|
||||
outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(0,86,179,0.1);
|
||||
}
|
||||
.btn-primary {
|
||||
padding: 0.75rem 2rem; background: var(--accent-color); border: none; border-radius: 4px;
|
||||
color: white; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s;
|
||||
}
|
||||
.btn-primary:hover { background: var(--hover-color); }
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.output {
|
||||
background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px;
|
||||
padding: 1rem; font-family: monospace; font-size: 0.85rem; word-break: break-all;
|
||||
line-height: 1.6; min-height: 60px; max-height: 120px; overflow-y: auto; color: var(--accent-color);
|
||||
}
|
||||
.output.error { color: #dc3545; }
|
||||
.output.copyable { cursor: pointer; position: relative; }
|
||||
.output.copyable:hover { border-color: var(--accent-color); }
|
||||
.copy-toast {
|
||||
position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%);
|
||||
background: var(--accent-color); color: #fff; padding: 0.5rem 1.25rem;
|
||||
border-radius: 6px; font-size: 0.85rem; font-weight: 500;
|
||||
opacity: 0; transition: opacity 0.25s; pointer-events: none; z-index: 9999;
|
||||
}
|
||||
.copy-toast.show { opacity: 1; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||
.loading { animation: pulse 1s infinite; }
|
||||
.eightball-answer {
|
||||
font-size: 1.3rem; font-weight: 600; text-align: center; padding: 1.5rem;
|
||||
border-radius: 50%; width: 180px; height: 180px; margin: 1rem auto;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: radial-gradient(circle at 40% 40%, #333, #111);
|
||||
color: #fff; font-family: serif; line-height: 1.3;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3); transition: transform 0.3s;
|
||||
}
|
||||
.eightball-answer:hover { transform: scale(1.05); }
|
||||
.eightball-answer.positive { color: #4cff72; }
|
||||
.eightball-answer.neutral { color: #ffd644; }
|
||||
.eightball-answer.negative { color: #ff6b6b; }
|
||||
@keyframes shake8ball {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
10% { transform: rotate(-8deg); } 20% { transform: rotate(8deg); }
|
||||
30% { transform: rotate(-6deg); } 40% { transform: rotate(6deg); }
|
||||
50% { transform: rotate(-3deg); } 60% { transform: rotate(3deg); }
|
||||
70% { transform: rotate(-1deg); } 80% { transform: rotate(1deg); }
|
||||
}
|
||||
.eightball-shaking { animation: shake8ball 0.6s ease-in-out; }
|
||||
@media (max-width: 600px) {
|
||||
body { padding: 10px; } h1 { font-size: 1.5em; }
|
||||
.controls { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="theme-switch">
|
||||
<button id="themeToggle" type="button" role="switch" aria-label="Theme mode: Auto">🌓</button>
|
||||
</div>
|
||||
|
||||
<a href="/" class="back-link">← Back to Camera TRNG</a>
|
||||
<h1>Quantum Random Tools</h1>
|
||||
<p class="subtitle">Dice, passwords, coin flips, and a Magic 8 Ball — all from quantum camera noise.</p>
|
||||
|
||||
<!-- Dice -->
|
||||
<div class="card">
|
||||
<h3>Quantum Dice</h3>
|
||||
<div class="controls">
|
||||
<div class="field">
|
||||
<label for="dice-sides">Sides (d)</label>
|
||||
<input type="number" id="dice-sides" value="6" min="2" max="256">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dice-count">Count</label>
|
||||
<input type="number" id="dice-count" value="1" min="1" max="100">
|
||||
</div>
|
||||
<button id="btn-dice" class="btn-primary">Roll</button>
|
||||
</div>
|
||||
<div class="output copyable" id="out-dice" title="Click to copy">Roll some dice...</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="card">
|
||||
<h3>Quantum Password</h3>
|
||||
<div class="controls">
|
||||
<div class="field">
|
||||
<label for="pw-length">Length</label>
|
||||
<input type="number" id="pw-length" value="16" min="1" max="128">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="pw-charset">Charset</label>
|
||||
<select id="pw-charset">
|
||||
<option value="alphanumeric">Alphanumeric</option>
|
||||
<option value="full">Full (+ symbols)</option>
|
||||
<option value="hex">Hex</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="display:flex;align-items:flex-end;gap:0.5rem;">
|
||||
<label for="pw-no-ambiguous" style="text-transform:none;white-space:nowrap;cursor:pointer;display:flex;align-items:center;gap:0.4rem;">
|
||||
<input type="checkbox" id="pw-no-ambiguous" checked>
|
||||
Exclude ambiguous <code style="font-size:0.8em;opacity:0.7;">0 O o I l 1 |</code>
|
||||
</label>
|
||||
</div>
|
||||
<button id="btn-password" class="btn-primary">Generate</button>
|
||||
</div>
|
||||
<div class="output copyable" id="out-password" title="Click to copy">Generate a password...</div>
|
||||
</div>
|
||||
|
||||
<!-- Coin -->
|
||||
<div class="card">
|
||||
<h3>Quantum Coin Flip</h3>
|
||||
<div class="controls">
|
||||
<button id="btn-coin" class="btn-primary">Flip</button>
|
||||
</div>
|
||||
<div class="output copyable" id="out-coin" title="Click to copy">Flip a coin...</div>
|
||||
</div>
|
||||
|
||||
<!-- 8 Ball -->
|
||||
<div class="card">
|
||||
<h3>Quantum 8 Ball</h3>
|
||||
<div class="controls">
|
||||
<div class="field" style="flex:3;">
|
||||
<label for="eightball-question">Ask the Quantum Void</label>
|
||||
<input type="text" id="eightball-question" placeholder="Will I ever understand quantum mechanics?" maxlength="200">
|
||||
</div>
|
||||
<button id="btn-eightball" class="btn-primary">Shake</button>
|
||||
</div>
|
||||
<div class="eightball-answer" id="eightball-orb" style="display:none;"></div>
|
||||
<div class="output copyable" id="out-eightball" title="Click to copy">Shake the 8 ball...</div>
|
||||
</div>
|
||||
|
||||
<div class="copy-toast" id="copy-toast">Copied!</div>
|
||||
|
||||
<script>
|
||||
// Theme toggle (shared with main page via localStorage)
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const html = document.documentElement;
|
||||
const saved = localStorage.getItem('theme') || 'auto';
|
||||
function setTheme(t) {
|
||||
const icons = { light: '\u{1F31E}', dark: '\u{1F319}', auto: '\u{1F313}' };
|
||||
themeToggle.textContent = icons[t];
|
||||
themeToggle.setAttribute('aria-label', 'Theme: ' + t);
|
||||
if (t === 'auto') html.removeAttribute('data-theme');
|
||||
else html.setAttribute('data-theme', t);
|
||||
}
|
||||
setTheme(saved);
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const cur = html.getAttribute('data-theme') || 'auto';
|
||||
const next = cur === 'light' ? 'dark' : cur === 'dark' ? 'auto' : 'light';
|
||||
setTheme(next); localStorage.setItem('theme', next);
|
||||
});
|
||||
|
||||
// Click-to-copy
|
||||
const copyToast = document.getElementById('copy-toast');
|
||||
let copyTimer;
|
||||
function copyText(el) {
|
||||
const t = el.textContent;
|
||||
if (!t || t.endsWith('...') || t.startsWith('Error:')) return;
|
||||
navigator.clipboard.writeText(t).then(() => {
|
||||
clearTimeout(copyTimer);
|
||||
copyToast.classList.add('show');
|
||||
copyTimer = setTimeout(() => copyToast.classList.remove('show'), 1500);
|
||||
});
|
||||
}
|
||||
|
||||
const origin = window.location.origin;
|
||||
const outDice = document.getElementById('out-dice');
|
||||
const outPw = document.getElementById('out-password');
|
||||
const outCoin = document.getElementById('out-coin');
|
||||
const outBall = document.getElementById('out-eightball');
|
||||
const orb = document.getElementById('eightball-orb');
|
||||
|
||||
// Wire click-to-copy on every output box
|
||||
[outDice, outPw, outCoin, outBall].forEach(el => el.addEventListener('click', () => copyText(el)));
|
||||
|
||||
const allBtns = ['btn-dice','btn-password','btn-coin','btn-eightball'].map(id => document.getElementById(id));
|
||||
function disableAll(v) { allBtns.forEach(b => b.disabled = v); }
|
||||
|
||||
// Dice
|
||||
document.getElementById('btn-dice').addEventListener('click', async () => {
|
||||
disableAll(true); outDice.className = 'output copyable loading'; outDice.textContent = 'Rolling...';
|
||||
try {
|
||||
const d = document.getElementById('dice-sides').value || 6;
|
||||
const c = document.getElementById('dice-count').value || 1;
|
||||
const r = await fetch(origin + '/dice?d=' + d + '&count=' + c);
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const j = await r.json();
|
||||
outDice.className = 'output copyable';
|
||||
outDice.textContent = 'Rolls: ' + j.rolls.join(', ') + (j.count > 1 ? ' Sum: ' + j.sum : '');
|
||||
} catch (e) { outDice.className = 'output copyable error'; outDice.textContent = 'Error: ' + e.message; }
|
||||
disableAll(false);
|
||||
});
|
||||
|
||||
// Password
|
||||
document.getElementById('btn-password').addEventListener('click', async () => {
|
||||
disableAll(true); outPw.className = 'output copyable loading'; outPw.textContent = 'Generating...';
|
||||
try {
|
||||
const len = document.getElementById('pw-length').value || 16;
|
||||
const cs = document.getElementById('pw-charset').value || 'alphanumeric';
|
||||
const noAmb = document.getElementById('pw-no-ambiguous').checked;
|
||||
let url = origin + '/password?length=' + len + '&charset=' + cs;
|
||||
if (noAmb) url += '&exclude_ambiguous=true';
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const j = await r.json();
|
||||
outPw.className = 'output copyable';
|
||||
outPw.style.fontFamily = "'SF Mono', Monaco, 'Cascadia Code', 'Fira Code', monospace";
|
||||
outPw.style.fontSize = '1.1rem';
|
||||
outPw.style.letterSpacing = '0.08em';
|
||||
outPw.textContent = j.password;
|
||||
} catch (e) { outPw.className = 'output copyable error'; outPw.textContent = 'Error: ' + e.message; }
|
||||
disableAll(false);
|
||||
});
|
||||
|
||||
// Coin
|
||||
document.getElementById('btn-coin').addEventListener('click', async () => {
|
||||
disableAll(true); outCoin.className = 'output copyable loading'; outCoin.textContent = 'Flipping...';
|
||||
try {
|
||||
const r = await fetch(origin + '/coin');
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const j = await r.json();
|
||||
outCoin.className = 'output copyable';
|
||||
outCoin.textContent = j.result.charAt(0).toUpperCase() + j.result.slice(1);
|
||||
} catch (e) { outCoin.className = 'output copyable error'; outCoin.textContent = 'Error: ' + e.message; }
|
||||
disableAll(false);
|
||||
});
|
||||
|
||||
// 8 Ball
|
||||
document.getElementById('btn-eightball').addEventListener('click', async () => {
|
||||
disableAll(true); outBall.className = 'output copyable loading'; outBall.textContent = '';
|
||||
orb.style.display = 'flex';
|
||||
orb.className = 'eightball-answer eightball-shaking'; orb.textContent = '...';
|
||||
try {
|
||||
const q = document.getElementById('eightball-question').value;
|
||||
let url = origin + '/8ball';
|
||||
if (q) url += '?question=' + encodeURIComponent(q);
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const j = await r.json();
|
||||
orb.className = 'eightball-answer ' + j.sentiment;
|
||||
orb.textContent = j.answer;
|
||||
outBall.className = 'output copyable';
|
||||
outBall.textContent = (q ? 'Q: ' + q + ' \u2192 ' : '') + j.answer;
|
||||
} catch (e) {
|
||||
orb.style.display = 'none';
|
||||
outBall.className = 'output copyable error'; outBall.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
disableAll(false);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue