feat(frontend): add Tefnut static page for issue #154#158
Conversation
|
@sameezy667 is attempting to deploy a commit to the djed-solidity Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughTwo new self-contained HTML pages implement a financial protocol dashboard UI. Each page includes market statistics, a protocol health gauge with dynamic updates, and a tabbed mint/redeem interaction interface with real-time calculation logic for exchanging between assets (ETH, DJED, SHEN). Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
generated_page.html (2)
312-312: Balance display is hardcoded and will not update dynamically.Line 312 shows a static balance hint:
Balance: 4.2 ETH. This is hardcoded and won't reflect the actual connected wallet's balance. When wallet integration is added, this must be dynamically updated via Web3 calls.For now, mark this as a placeholder:
<span class="text-xs text-orange-600 cursor-pointer hover:underline font-medium"> Balance: <span id="wallet-balance">4.2</span> ETH </span>Then update via contract query:
// TODO: Fetch from wallet provider (ethers.js / Web3.js) document.getElementById('wallet-balance').innerText = userBalance;
499-520: Document magic numbers in gauge animation logic.Lines 505–510 compute the SVG
stroke-dashoffsetusing undocumented approximations:const percentage = (currentRatio - 400) / 400; // 0 to 1 const offset = 251 - (200 * percentage); // ???The constant
251.2(from line 225,stroke-dasharray) and the offset formula251 - (200 * percentage)lack explanation. It's unclear if this correctly maps the reserve ratio (400%–800%) to the visual gauge fill.Add comments explaining the mapping:
// SVG stroke-dasharray is ~251 (full arc); we map reserve ratio to visible arc // Range: 400% (empty) → 800% (full), normalized to 0–1 const percentage = (currentRatio - 400) / 400; // 0 = min ratio (empty), 1 = max ratio (full) // Invert offset: at 0%, offset=251 (no fill); at 100%, offset ≈51 (full fill) const maxOffset = 251.2; // dasharray length (approximate) const offset = maxOffset - (maxOffset * percentage);Verify the calculation matches the intended visual behavior by testing edge cases (currentRatio = 400, 600, 800).
public/tefnut.html (1)
1-1: Canonical page location: Clarify ifpublic/tefnut.htmlis the primary version for Next.js conversion.This file is located in the
public/folder and appears to be the primary page. Per the PR description,tefnut.htmlis labeled as the "main preserved/generated page for review." When converting to Next.js/React, this should become the single canonical source (e.g.,src/pages/tefnut.jsxorapp/tefnut/page.tsx), andgenerated_page.htmlshould be deleted to eliminate duplication.In the meantime, ensure this file is treated as the reference version for all updates and refinements.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
generated_page.html(1 hunks)public/tefnut.html(1 hunks)
🔇 Additional comments (1)
generated_page.html (1)
391-396: Hardcoded prices will not reflect contract state; plan dynamic fetching.The
PRICESobject (lines 391–396) andFEESconstant are hardcoded to static values. In a production system, these must be fetched from the blockchain contract in real time. Even for a prototype, this should be called out as a placeholder.For the static prototype, this is acceptable, but document it clearly and plan the integration path:
// TODO: Replace with dynamic contract queries (Web3.js / ethers.js) const PRICES = { ETH: 2042.50, // Fetch from price oracle or DEX DJED: 1.00, // Query contract reserve ratio SHEN: 2.45 // Fetch from price oracle }; const FEES = 0.015; // Query from contractAdd a comment block above these constants noting they are placeholder values for the static prototype and must be replaced with contract calls before production conversion.
When converting to Next.js/React, ensure you integrate with:
- A price oracle (e.g., Chainlink, Uniswap TWAP) for ETH and SHEN prices.
- Direct contract queries for DJED price and reserve ratio.
- Real wallet balance and transaction fee estimation.
| <html lang="en"><head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Djed Tefnut | Financial Protocol</title> | ||
| <script src="https://cdn.tailwindcss.com"></script> | ||
| <script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script> | ||
| <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=Inter:wght@300;400;500;600&family=Lora:ital,wght@0,400;0,500;0,600;1,400&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"> | ||
|
|
There was a problem hiding this comment.
Eliminate duplicate static HTML files; consolidate to single source.
Both generated_page.html and public/tefnut.html are 99% identical (~524 lines each). This violates DRY and creates a significant maintenance burden: any UI or logic change must be applied twice, and inconsistencies will emerge.
Per the PR description, this PR includes both a "main preserved/generated page for review" and "backup / original copy." Clarify which is the canonical version and delete the other. As you convert this to Next.js/React, this duplication should be eliminated entirely in favor of a single component/page definition.
🤖 Prompt for AI Agents
In generated_page.html around lines 1 to 10: there is a near-identical duplicate
of public/tefnut.html; choose one canonical source (either keep
generated_page.html or public/tefnut.html), delete the other copy, and update
any references/build scripts to point to the retained file. If this is being
converted to Next.js/React, replace the retained static file with a single React
component/page and remove the duplicate static asset, ensuring the router/build
uses only the new component; also run a quick search to remove any remaining
references to the deleted file.
| <span class="text-xs text-orange-600 cursor-pointer hover:underline font-medium">Balance: 4.2 ETH</span> | ||
| </div> | ||
| <div class="relative group"> | ||
| <input type="number" id="amount-in" placeholder="0.00" class="w-full bg-white border border-gray-200 rounded-lg pl-4 pr-24 py-4 text-xl font-mono text-slate-900 placeholder-slate-300 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all shadow-sm" oninput="updateCalculation()"> |
There was a problem hiding this comment.
Add input validation and error handling for amount inputs.
The amount-in input (line 315) accepts any numeric value without bounds checking or validation feedback. The calculation logic (lines 457–492) handles NaN gracefully (lines 461–465), but there are missing safeguards:
- No upper bound: User can enter arbitrarily large numbers; no warning if it exceeds wallet balance (which is hardcoded at 4.2 ETH anyway).
- No decimal precision limit: Depending on token decimals, users could enter amounts that cause rounding errors or precision loss.
- Silent failures: If
updateCalculation()encounters an error, there's no user-facing error message or validation feedback.
Add client-side validation and provide clear feedback:
function validateAmount(val, maxBalance = 4.2) {
if (isNaN(val) || val < 0) return { valid: false, error: "Invalid amount" };
if (val > maxBalance) return { valid: false, error: "Insufficient balance" };
if (!/^\d+\.?\d{0,8}$/.test(val.toString())) return { valid: false, error: "Too many decimals" };
return { valid: true };
}
// Use in updateCalculation():
const val = parseFloat(elAmountIn.value);
const validation = validateAmount(val);
if (!validation.valid) {
elAmountOut.value = "";
elRateDisplay.innerText = validation.error;
return;
}Display validation errors clearly in the UI (e.g., below the input or in a toast).
Also applies to: 335-335
🤖 Prompt for AI Agents
In generated_page.html around lines 315 and 335, the amount-in input needs
client-side validation and user-facing error handling: add a validateAmount
function that checks NaN, negative values, an upper bound (use the 4.2 ETH
maxBalance or a configurable constant), and a decimal-precision limit (e.g., max
8 decimals); call this validator at the start of updateCalculation(), and if
invalid clear the output, display the validation error text in a visible UI
element (or inline message below the input) and apply an error style to the
input, and wrap updateCalculation logic in a try/catch that shows a friendly
error message on exception so calculations never fail silently.
| <script> | ||
| // --- Bank Logic --- | ||
|
|
||
| // Constants | ||
| const PRICES = { | ||
| ETH: 2042.50, | ||
| DJED: 1.00, | ||
| SHEN: 2.45 | ||
| }; | ||
| const FEES = 0.015; // 1.5% | ||
|
|
||
| // State | ||
| let currentTab = 'mint'; | ||
| let currentToken = 'DJED'; | ||
|
|
||
| // Elements | ||
| const elAmountIn = document.getElementById('amount-in'); | ||
| const elAmountOut = document.getElementById('amount-out'); | ||
| const elTickerIn = document.getElementById('ticker-in'); | ||
| const elTickerOut = document.getElementById('ticker-out'); | ||
| const elRateDisplay = document.getElementById('rate-display'); | ||
| const elActionBtn = document.getElementById('action-btn'); | ||
| const elInputLabel = document.getElementById('input-label'); | ||
| const elOutputLabel = document.getElementById('output-label'); | ||
| const btnMint = document.getElementById('tab-mint'); | ||
| const btnRedeem = document.getElementById('tab-redeem'); | ||
|
|
||
| function switchTab(tab) { | ||
| currentTab = tab; | ||
|
|
||
| // Style Updates | ||
| if(tab === 'mint') { | ||
| btnMint.classList.remove('text-slate-500', 'border-transparent', 'hover:bg-gray-50'); | ||
| btnMint.classList.add('text-orange-600', 'border-orange-500', 'bg-orange-50/50'); | ||
|
|
||
| btnRedeem.classList.add('text-slate-500', 'border-transparent', 'hover:bg-gray-50'); | ||
| btnRedeem.classList.remove('text-orange-600', 'border-orange-500', 'bg-orange-50/50'); | ||
|
|
||
| elActionBtn.innerHTML = `Connect Wallet to Mint <iconify-icon icon="lucide:arrow-right" width="16"></iconify-icon>`; | ||
| elInputLabel.innerText = "You Pay"; | ||
| } else { | ||
| btnRedeem.classList.remove('text-slate-500', 'border-transparent', 'hover:bg-gray-50'); | ||
| btnRedeem.classList.add('text-orange-600', 'border-orange-500', 'bg-orange-50/50'); | ||
|
|
||
| btnMint.classList.add('text-slate-500', 'border-transparent', 'hover:bg-gray-50'); | ||
| btnMint.classList.remove('text-orange-600', 'border-orange-500', 'bg-orange-50/50'); | ||
|
|
||
| elActionBtn.innerHTML = `Connect Wallet to Redeem <iconify-icon icon="lucide:arrow-right" width="16"></iconify-icon>`; | ||
| elInputLabel.innerText = "You Burn"; | ||
| } | ||
|
|
||
| updateLabels(); | ||
| updateCalculation(); | ||
| } | ||
|
|
||
| function updateLabels() { | ||
| const radios = document.getElementsByName('token'); | ||
| for (let radio of radios) { | ||
| if (radio.checked) currentToken = radio.value; | ||
| } | ||
|
|
||
| if (currentTab === 'mint') { | ||
| elTickerIn.innerText = "ETH"; | ||
| elTickerOut.innerText = currentToken; | ||
| } else { | ||
| elTickerIn.innerText = currentToken; | ||
| elTickerOut.innerText = "ETH"; | ||
| } | ||
| } | ||
|
|
||
| function updateCalculation() { | ||
| updateLabels(); | ||
| const val = parseFloat(elAmountIn.value); | ||
|
|
||
| if (isNaN(val) || val <= 0) { | ||
| elAmountOut.value = ""; | ||
| elRateDisplay.innerText = "-"; | ||
| return; | ||
| } | ||
|
|
||
| let result = 0; | ||
| let rateText = ""; | ||
|
|
||
| if (currentTab === 'mint') { | ||
| // ETH -> Token | ||
| const ethValue = val * PRICES.ETH; | ||
| const tokenPrice = PRICES[currentToken]; | ||
| const rawAmount = ethValue / tokenPrice; | ||
| const finalAmount = rawAmount * (1 - FEES); | ||
|
|
||
| result = finalAmount; | ||
| rateText = `1 ETH = ${(PRICES.ETH / tokenPrice).toFixed(2)} ${currentToken}`; | ||
|
|
||
| } else { | ||
| // Token -> ETH | ||
| const tokenValue = val * PRICES[currentToken]; | ||
| const rawEth = tokenValue / PRICES.ETH; | ||
| const finalEth = rawEth * (1 - FEES); | ||
|
|
||
| result = finalEth; | ||
| rateText = `1 ${currentToken} = ${(PRICES[currentToken] / PRICES.ETH).toFixed(6)} ETH`; | ||
| } | ||
|
|
||
| elAmountOut.value = result.toFixed(4); | ||
| elRateDisplay.innerText = rateText; | ||
| } | ||
|
|
||
| // --- Gauge Animation --- | ||
| const gaugePath = document.getElementById('health-gauge'); | ||
| const ratioDisplay = document.getElementById('ratio-display'); | ||
| let currentRatio = 546; | ||
|
|
||
| setInterval(() => { | ||
| const change = (Math.random() - 0.5) * 8; | ||
| currentRatio = Math.max(400, Math.min(800, currentRatio + change)); | ||
|
|
||
| ratioDisplay.innerText = Math.floor(currentRatio) + "%"; | ||
|
|
||
| // Max dasharray is approx 251 (2 * PI * 40 - partial circle logic) | ||
| // Let's approximate the stroke-dashoffset logic for the SVG path | ||
| // Range 400% to 800% | ||
| const percentage = (currentRatio - 400) / 400; // 0 to 1 | ||
| // Invert logic for dashoffset: 251 is empty, 0 is full (roughly) | ||
| const offset = 251 - (200 * percentage); | ||
|
|
||
| gaugePath.style.strokeDashoffset = offset; | ||
|
|
||
| // Visual warning color | ||
| if(currentRatio < 420) { | ||
| gaugePath.setAttribute('stroke', '#ef4444'); // Red warning | ||
| } else { | ||
| gaugePath.setAttribute('stroke', '#f97316'); // Orange normal | ||
| } | ||
| }, 3000); | ||
|
|
||
| </script> |
There was a problem hiding this comment.
Fix memory leak: setInterval has no cleanup handler.
Line 499 starts a setInterval that runs every 3 seconds indefinitely. If the page is navigated away or the component is unmounted (especially important when converting to React/Next.js), the interval persists in memory and continues polling, consuming resources and potentially causing stale state mutations.
Store the interval ID and clear it on page unload or, preferably, wrap this in a page lifecycle handler.
// Current (lines 499-520):
setInterval(() => {
// gauge update logic
}, 3000);
// Proposed fix (for now, for static HTML):
const gaugeIntervalId = setInterval(() => {
// gauge update logic
}, 3000);
// Add cleanup on page unload:
window.addEventListener('beforeunload', () => {
clearInterval(gaugeIntervalId);
});When converting to React/Next.js, use useEffect with cleanup:
useEffect(() => {
const id = setInterval(() => { /* ... */ }, 3000);
return () => clearInterval(id);
}, []);🤖 Prompt for AI Agents
In generated_page.html around lines 387 to 522, the setInterval started at line
~499 is never cleared causing a memory leak; capture the interval ID when
creating it and call clearInterval on page unload (e.g., window.beforeunload or
unload) to stop it, or if migrating to React/Next.js move the logic into a
useEffect and return a cleanup function that clears the interval; ensure the
interval ID is stored in a variable with appropriate scope so the cleanup can
reference it.
| btnRedeem.classList.add('text-slate-500', 'border-transparent', 'hover:bg-gray-50'); | ||
| btnRedeem.classList.remove('text-orange-600', 'border-orange-500', 'bg-orange-50/50'); | ||
|
|
||
| elActionBtn.innerHTML = `Connect Wallet to Mint <iconify-icon icon="lucide:arrow-right" width="16"></iconify-icon>`; |
There was a problem hiding this comment.
Avoid dynamic innerHTML for UI elements; use safer patterns.
Lines 425 and 434 use innerHTML to dynamically set button text with an embedded <iconify-icon> tag:
elActionBtn.innerHTML = `Connect Wallet to Mint <iconify-icon icon="lucide:arrow-right" width="16"></iconify-icon>`;While the Iconify library is from a trusted CDN, using innerHTML with template strings is generally unsafe and can introduce XSS if user input or untrusted data ever enters the template. Additionally, replacing the entire HTML content destroys any event listeners attached to child nodes.
Safer approach: Update only the text content, or append pre-built elements:
// Option 1: Update text only, preserve iconify element
const textNode = elActionBtn.querySelector('span:first-child');
textNode.innerText = 'Connect Wallet to Mint';
// Option 2: Use textContent + createElement for dynamic elements
elActionBtn.textContent = 'Connect Wallet to Mint ';
const icon = document.createElement('iconify-icon');
icon.setAttribute('icon', 'lucide:arrow-right');
icon.setAttribute('width', '16');
elActionBtn.appendChild(icon);Also applies to: 434-434
🤖 Prompt for AI Agents
In generated_page.html around lines 425 and 434, avoid using innerHTML to set
the button content; instead preserve existing child nodes and event listeners by
updating only the button text (e.g., update a child span's textContent) or by
setting the button's textContent and creating a new icon element via
document.createElement('iconify-icon'), set its attributes (icon and width) and
append it to the button; ensure you do not use string templates for HTML
injection and that any existing listeners on child nodes are not removed.
Summary: Add an initial static frontend page for the Djed Tefnut frontend (Sepolia testnet preview / visual reference) to help implement the Tefnut specification and speed up frontend integration for issue #154.
What I changed
Added tefnut.html — main preserved/generated page for review.
Added generated_page.html — backup / original copy.
Branch: sameezy667/tefnut-frontend
Why
Provide a ready HTML/CSS/JS prototype of the Tefnut UI so maintainers can review the visual/layout, and developers can convert it into the repository's React/Next pages.
Preserves the page you supplied so we don't lose the original iteration.
How to preview
Install dependencies and run the dev server:
Or preview the static file directly by opening tefnut.html in a browser.
Notes / Next steps
These are static HTML files and are not yet wired into the Next.js app. Next actions I recommend:
Convert the page into a Next.js route (e.g., src/pages/tefnut.jsx or app/tefnut/page.tsx) and reuse existing layout/components.
Integrate wallet connection + contract interaction to match the Tefnut spec.
Add tests/visual regression and accessibility checks.
This PR is intended as a first step toward implementing Djed Tefnut frontend per issue
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.