Created
August 4, 2025 08:24
-
-
Save taksvj/7d620aa254acdf853216ebb4b714611b to your computer and use it in GitHub Desktop.
Untitled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>EthOS $AIR Perpetual Game Preview</title> | |
<script src="https://unpkg.com/react@18/umd/react.development.js"></script> | |
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> | |
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
<style> | |
body { | |
margin: 0; | |
background: #c0c0e0 url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFklEQVQoU2P8z/D/PwMDAwNjAwMDAwMDIxsDAwMDP8AAGqQSuAAAAAElFTkSuQmCC') repeat; | |
font-family: 'Courier New', monospace; | |
} | |
.App { | |
color: #000000; | |
min-height: 100vh; | |
text-align: center; | |
padding: 20px 0; | |
} | |
h1 { | |
font-size: 1.8rem; | |
margin-bottom: 0.5em; | |
font-weight: bold; | |
color: #000080; | |
background: #c0c0c0; | |
padding: 5px; | |
border: 2px outset #ffffff; | |
display: inline-block; | |
} | |
.balance { | |
font-size: 1rem; | |
margin-bottom: 0.5em; | |
background: #c0c0c0; | |
padding: 5px; | |
border: 2px outset #ffffff; | |
display: inline-block; | |
} | |
.current-price { | |
font-size: 1.1rem; | |
margin-bottom: 0.5em; | |
font-weight: bold; | |
background: #c0c0c0; | |
padding: 5px; | |
border: 2px outset #ffffff; | |
display: inline-block; | |
} | |
.profit-display { | |
font-size: 1rem; | |
font-weight: bold; | |
margin-bottom: 0.5em; | |
background: #c0c0c0; | |
padding: 5px; | |
border: 2px outset #ffffff; | |
display: inline-block; | |
} | |
.round-info { | |
font-size: 0.9rem; | |
margin-bottom: 0.5em; | |
background: #c0c0c0; | |
padding: 5px; | |
border: 2px outset #ffffff; | |
display: inline-block; | |
} | |
.controls { | |
margin: 10px 0; | |
display: flex; | |
gap: 8px; | |
justify-content: center; | |
align-items: center; | |
flex-wrap: wrap; | |
} | |
.bet-btn { | |
font-size: 0.9rem; | |
padding: 0.4em 1em; | |
margin: 0 0.1em; | |
border: 2px outset #ffffff; | |
background: #808080; | |
color: #000000; | |
cursor: pointer; | |
font-weight: bold; | |
transition: border 0.2s; | |
} | |
.bet-btn:disabled { | |
background: #a0a0a0; | |
color: #404040; | |
cursor: default; | |
border: 2px solid #808080; | |
} | |
.bet-btn.up.selected, | |
.bet-btn.up:hover:not(:disabled) { | |
background: #00ff00; | |
color: #000000; | |
border: 2px outset #ffffff; | |
} | |
.bet-btn.down.selected, | |
.bet-btn.down:hover:not(:disabled) { | |
background: #ff0000; | |
color: #000000; | |
border: 2px outset #ffffff; | |
} | |
.start-btn { | |
font-size: 0.9rem; | |
background: #00ff00; | |
color: #000000; | |
padding: 0.4em 1.2em; | |
border: 2px outset #ffffff; | |
font-weight: bold; | |
cursor: pointer; | |
transition: background 0.2s; | |
} | |
.start-btn:disabled { | |
background: #a0a0a0; | |
color: #404040; | |
border: 2px solid #808080; | |
cursor: default; | |
} | |
.cashout-btn { | |
font-size: 0.9rem; | |
background: #ffff00; | |
color: #000000; | |
padding: 0.4em 1.2em; | |
border: 2px outset #ffffff; | |
font-weight: bold; | |
cursor: pointer; | |
transition: background 0.2s; | |
} | |
.cashout-btn:hover { | |
background: #ffff80; | |
} | |
input[type="number"] { | |
font-size: 0.9rem; | |
padding: 0.3em 0.5em; | |
border: 2px inset #808080; | |
background: #ffffff; | |
color: #000000; | |
font-family: 'Courier New', monospace; | |
} | |
.history { | |
max-width: 480px; | |
margin: 20px auto 0 auto; | |
background: #c0c0c0; | |
border: 2px outset #ffffff; | |
padding: 10px; | |
} | |
.history h3 { | |
margin-bottom: 0.5em; | |
color: #000080; | |
font-weight: bold; | |
} | |
.history table { | |
width: 100%; | |
border-collapse: collapse; | |
} | |
.history th, .history td { | |
font-size: 0.8rem; | |
padding: 0.2em 0.3em; | |
border: 1px solid #808080; | |
} | |
.history th { | |
background: #000080; | |
color: #ffffff; | |
font-weight: bold; | |
} | |
.history td { | |
background: #ffffff; | |
color: #000000; | |
} | |
.modal-backdrop { | |
position: fixed; | |
left: 0; top: 0; right: 0; bottom: 0; | |
background: #00000080; | |
z-index: 200; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.modal { | |
background: #c0c0c0; | |
color: #000000; | |
border: 2px outset #ffffff; | |
padding: 20px; | |
box-shadow: 0 4px 32px #00000040; | |
min-width: 260px; | |
} | |
.modal h2 { | |
font-size: 1.5rem; | |
margin-bottom: 0.5em; | |
color: #000080; | |
} | |
.modal button { | |
font-size: 0.9rem; | |
background: #00ff00; | |
color: #000000; | |
padding: 0.4em 1.2em; | |
border: 2px outset #ffffff; | |
font-weight: bold; | |
cursor: pointer; | |
} | |
.tab { | |
background: #c0c0c0; | |
border: 2px outset #ffffff; | |
padding: 5px 10px; | |
margin: 5px; | |
display: inline-block; | |
cursor: default; | |
} | |
.tab-content { | |
background: #ffffff; | |
border: 2px inset #808080; | |
padding: 10px; | |
margin-top: 5px; | |
display: none; | |
} | |
.tab-content.active { | |
display: block; | |
} | |
/* Animasi untuk Chart */ | |
.chart-path { | |
animation: draw 1s linear forwards; | |
} | |
@keyframes draw { | |
from { | |
stroke-dasharray: 1000; | |
stroke-dashoffset: 1000; | |
} | |
to { | |
stroke-dasharray: 1000; | |
stroke-dashoffset: 0; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div id="root"></div> | |
<!-- Audio Files --> | |
<audio id="startSound" src="https://www.soundjay.com/buttons/beep-01a.mp3"></audio> | |
<audio id="cashoutSound" src="https://www.soundjay.com/buttons/coin-01a.mp3"></audio> | |
<audio id="endSound" src="https://www.soundjay.com/buttons/beep-03.mp3"></audio> | |
<script type="text/babel"> | |
const { useState, useEffect, useRef } = React; | |
// KOMPONEN CHART | |
function Chart({ priceSeries }) { | |
const width = 300; | |
const height = 100; | |
if (priceSeries.length < 2) return <div style={{ height, width, border: "2px inset #808080", background: "#ffffff", marginBottom: 10, margin: 'auto' }} />; | |
const min = Math.min(...priceSeries); | |
const max = Math.max(...priceSeries); | |
const range = max - min || 1; | |
const points = priceSeries.map((p, i) => [ | |
(i / (priceSeries.length - 1)) * width, | |
height - ((p - min) / range) * (height - 10) - 5, | |
]); | |
const path = points | |
.map((pt, i) => (i === 0 ? `M${pt[0]},${pt[1]}` : `L${pt[0]},${pt[1]}`)) | |
.join(" "); | |
const totalLength = points.length * 10; // Estimasi panjang garis | |
return ( | |
<svg | |
width={width} | |
height={height} | |
style={{ | |
border: "2px inset #808080", | |
background: "#ffffff", | |
marginBottom: 10, | |
}} | |
> | |
<path | |
className="chart-path" | |
d={path} | |
fill="none" | |
stroke="#000080" | |
strokeWidth={2} | |
strokeLinejoin="round" | |
style={{ strokeDasharray: totalLength, strokeDashoffset: totalLength }} | |
/> | |
</svg> | |
); | |
} | |
// KOMPONEN CONTROLS | |
function Controls({ bet, setBet, betAmount, setBetAmount, canBet, onStart, onCashOut, inRound, balance }) { | |
return ( | |
<div className="controls"> | |
<div> | |
<button | |
className={`bet-btn up ${bet === "UP" ? "selected" : ""}`} | |
disabled={inRound} | |
onClick={() => setBet("UP")} | |
aria-label="Bet on price going up" | |
> | |
UP | |
</button> | |
<button | |
className={`bet-btn down ${bet === "DOWN" ? "selected" : ""}`} | |
disabled={inRound} | |
onClick={() => setBet("DOWN")} | |
aria-label="Bet on price going down" | |
> | |
DOWN | |
</button> | |
</div> | |
<div> | |
<input | |
type="number" | |
min={1} | |
max={balance} | |
value={betAmount} | |
disabled={inRound} | |
onChange={e => { | |
const value = Math.max(1, Math.min(balance, Number(e.target.value) || 1)); | |
setBetAmount(value); | |
}} | |
style={{ width: 80, marginRight: 8 }} | |
aria-label="Bet amount in USD" | |
/> USD | |
</div> | |
{inRound ? ( | |
<button className="cashout-btn" onClick={onCashOut} aria-label="Cash out current bet"> | |
Cash Out | |
</button> | |
) : ( | |
<button className="start-btn" onClick={onStart} disabled={!canBet} aria-label="Start the betting round"> | |
Start | |
</button> | |
)} | |
</div> | |
); | |
} | |
// KOMPONEN HISTORY | |
function History({ history }) { | |
if (!history.length) return null; | |
return ( | |
<div className="history"> | |
<h3>History</h3> | |
<table> | |
<thead> | |
<tr> | |
<th>Round</th> | |
<th>Bet</th> | |
<th>Amount</th> | |
<th>Start</th> | |
<th>End</th> | |
<th>P/L</th> | |
</tr> | |
</thead> | |
<tbody> | |
{history.map((h) => ( | |
<tr key={h.round}> | |
<td>{h.round}</td> | |
<td>{h.bet}</td> | |
<td>${h.betAmount}</td> | |
<td>${h.priceStart.toFixed(4)}</td> | |
<td>${h.priceEnd.toFixed(4)}</td> | |
<td style={{ color: h.profit > 0 ? "#00ff00" : h.profit < 0 ? "#ff0000" : "#000000" }}> | |
{h.profit > 0 ? "+" : ""} | |
{h.profit.toFixed(2)} | |
</td> | |
</tr> | |
))} | |
</tbody> | |
</table> | |
</div> | |
); | |
} | |
// KOMPONEN RESULT MODAL | |
function ResultModal({ open, result, priceStart, priceEnd, bet, betAmount, onClose }) { | |
if (!open) return null; | |
return ( | |
<div className="modal-backdrop"> | |
<div className="modal" role="dialog" aria-labelledby="modal-title"> | |
<h2 id="modal-title">{result === "WIN" ? "🎉 You Win!" : result === "LOSE" ? "😔 You Lose" : "😐 Draw"}</h2> | |
<div> | |
Bet: <strong>{bet}</strong> | |
</div> | |
<div> | |
Bet Amount: <strong>${betAmount}</strong> | |
</div> | |
<div> | |
Start Price: <strong>${priceStart?.toFixed(4)}</strong> | |
</div> | |
<div> | |
End Price: <strong>${priceEnd?.toFixed(4)}</strong> | |
</div> | |
<button onClick={onClose} aria-label="Proceed to next round">Next Round</button> | |
</div> | |
</div> | |
); | |
} | |
// KOMPONEN UTAMA (APP) | |
function App() { | |
const START_BALANCE = 100; | |
const ROUND_DURATION = 15; | |
const [balance, setBalance] = useState(START_BALANCE); | |
const [currentPrice, setCurrentPrice] = useState(0.007); | |
const [priceSeries, setPriceSeries] = useState([0.007]); | |
const [bet, setBet] = useState(null); | |
const [betAmount, setBetAmount] = useState(10); | |
const [round, setRound] = useState(1); | |
const [timer, setTimer] = useState(ROUND_DURATION); | |
const [inRound, setInRound] = useState(false); | |
const [priceStart, setPriceStart] = useState(null); | |
const [priceEnd, setPriceEnd] = useState(null); | |
const [showResult, setShowResult] = useState(false); | |
const [result, setResult] = useState(null); | |
const [currentProfit, setCurrentProfit] = useState(0); | |
const [history, setHistory] = useState([]); | |
const intervalRef = useRef(null); | |
const timerRef = useRef(null); | |
const getRandomPrice = (base) => { | |
return +(base + (Math.random() - 0.5) * 0.0005).toFixed(4); | |
}; | |
useEffect(() => { | |
if (!inRound) return; | |
intervalRef.current = setInterval(() => { | |
setCurrentPrice((last) => { | |
const next = getRandomPrice(last); | |
setPriceSeries((ps) => [...ps.slice(-29), next].slice(-30)); | |
if (bet && priceStart !== null) { | |
let profit = -betAmount; | |
const priceDiff = next - priceStart; | |
if ((bet === "UP" && priceDiff > 0) || (bet === "DOWN" && priceDiff < 0)) { | |
profit = betAmount * Math.min(2, Math.abs(priceDiff) / priceStart); | |
} else if (priceDiff === 0) { | |
profit = 0; | |
} | |
setCurrentProfit(profit); | |
} | |
return next; | |
}); | |
}, 700); | |
return () => clearInterval(intervalRef.current); | |
}, [inRound, bet, priceStart, betAmount]); | |
useEffect(() => { | |
if (!inRound) return; | |
if (timer === 0) { | |
endRound(false); | |
return; | |
} | |
timerRef.current = setTimeout(() => setTimer((s) => s - 1), 1000); | |
return () => clearTimeout(timerRef.current); | |
}, [timer, inRound]); | |
const startRound = () => { | |
if (inRound || !bet || betAmount <= 0 || betAmount > balance) return; | |
setBalance(b => b - betAmount); | |
setInRound(true); | |
setTimer(ROUND_DURATION); | |
setPriceStart(currentPrice); | |
setPriceEnd(null); | |
setPriceSeries([currentPrice]); | |
setShowResult(false); | |
setResult(null); | |
setCurrentProfit(0); | |
document.getElementById('startSound').play(); // Putar suara saat ronde dimulai | |
}; | |
const endRound = (isCashOut) => { | |
setInRound(false); | |
clearTimeout(timerRef.current); | |
const finalPrice = currentPrice; | |
setPriceEnd(finalPrice); | |
let finalProfit = 0; | |
if (isCashOut) { | |
finalProfit = currentProfit; | |
document.getElementById('cashoutSound').play(); // Putar suara saat cash out | |
} else { | |
const priceDiff = finalPrice - priceStart; | |
if ((bet === "UP" && priceDiff > 0) || (bet === "DOWN" && priceDiff < 0)) { | |
finalProfit = betAmount; | |
} else if (priceDiff === 0) { | |
finalProfit = 0; | |
} else { | |
finalProfit = -betAmount; | |
} | |
} | |
const totalReturn = betAmount + finalProfit; | |
if (totalReturn > 0) { | |
setBalance(b => b + totalReturn); | |
} | |
setResult(finalProfit > 0 ? "WIN" : finalProfit < 0 ? "LOSE" : "DRAW"); | |
setShowResult(true); | |
document.getElementById('endSound').play(); // Putar suara saat ronde berakhir | |
setHistory((h) => [ | |
{ | |
round, | |
bet, | |
betAmount, | |
outcome: finalProfit > 0 ? "WIN" : finalProfit < 0 ? "LOSE" : "DRAW", | |
priceStart, | |
priceEnd: finalPrice, | |
profit: finalProfit, | |
}, | |
...h.slice(0, 9), | |
]); | |
setRound((r) => r + 1); | |
}; | |
const handleCashOut = () => { | |
if (!inRound) return; | |
endRound(true); | |
}; | |
const handleNext = () => { | |
setShowResult(false); | |
setBet(null); | |
setBetAmount(10); | |
setPriceSeries([currentPrice]); | |
setPriceStart(null); | |
setPriceEnd(null); | |
setCurrentProfit(0); | |
}; | |
return ( | |
<div className="App"> | |
<div className="tab">EthOS $AIR Perpetual</div> | |
<div className="tab-content active"> | |
<div className="balance">Balance: ${balance.toFixed(2)}</div> | |
<Chart priceSeries={priceSeries} /> | |
<div className="current-price">Current Price: ${currentPrice.toFixed(4)}</div> | |
{inRound && ( | |
<div className="profit-display" style={{ color: currentProfit >= 0 ? '#00ff00' : '#ff0000' }}> | |
P/L: {currentProfit >= 0 ? '+' : ''}${currentProfit.toFixed(2)} | |
</div> | |
)} | |
<div className="round-info"> | |
Round: {round} {inRound && `(Ends in ${timer}s)`} | |
</div> | |
<Controls | |
bet={bet} | |
setBet={setBet} | |
betAmount={betAmount} | |
setBetAmount={setBetAmount} | |
canBet={!inRound && betAmount > 0 && betAmount <= balance && bet !== null} | |
onStart={startRound} | |
onCashOut={handleCashOut} | |
inRound={inRound} | |
balance={balance} | |
/> | |
<History history={history} /> | |
<ResultModal | |
open={showResult} | |
result={result} | |
priceStart={priceStart} | |
priceEnd={priceEnd} | |
bet={bet} | |
betAmount={betAmount} | |
onClose={handleNext} | |
/> | |
</div> | |
</div> | |
); | |
} | |
const container = document.getElementById('root'); | |
const root = ReactDOM.createRoot(container); | |
root.render(<App />); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment