366 lines
16 KiB
PHP
366 lines
16 KiB
PHP
<?php
|
|
require_once __DIR__ . "/includes/lang.php";
|
|
require_once __DIR__ . "/includes/header.php";
|
|
|
|
$balances = [];
|
|
if ($user) {
|
|
$stmt = db()->prepare("SELECT symbol, available FROM user_balances WHERE user_id = ?");
|
|
$stmt->execute([$user["id"]]);
|
|
while ($row = $stmt->fetch()) {
|
|
$balances[$row["symbol"]] = (float)$row["available"];
|
|
}
|
|
}
|
|
?>
|
|
<div class="container py-4 py-md-5 d-flex justify-content-center">
|
|
<div class="card bg-dark border-0 shadow-lg" style="width: 100%; max-width: 480px; border-radius: 28px; background: #0b0e11 !important; border: 1px solid #2b3139 !important; box-shadow: 0 20px 50px rgba(0,0,0,0.5) !important;">
|
|
<div class="card-body p-3 p-md-4">
|
|
<h3 class="fw-bold mb-4 text-white d-flex align-items-center gap-2">
|
|
<i class="bi bi-lightning-charge-fill text-primary"></i>
|
|
<?= __("swap") ?>
|
|
</h3>
|
|
|
|
<!-- From -->
|
|
<div class="p-3 p-md-4 mb-2 rounded-4" style="background: #161a1e; border: 1px solid #2b3139;">
|
|
<div class="d-flex justify-content-between mb-3">
|
|
<span class="text-white opacity-50 small fw-bold text-uppercase" style="letter-spacing: 1px;"><?= __("from") ?></span>
|
|
<span class="text-white opacity-50 small"><?= __("balance") ?>: <span class="text-white fw-bold" id="from-balance-val">0.00</span></span>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<input type="number" id="from-amount" class="form-control bg-transparent border-0 text-white p-0 shadow-none w-50 fw-bold" placeholder="0.00" style="color: #fff !important; font-size: clamp(24px, 5vw, 32px) !important;">
|
|
<div class="ms-auto dropdown">
|
|
<div class="d-flex align-items-center bg-dark p-2 rounded-pill px-2 px-md-3 border border-secondary cursor-pointer shadow-sm dropdown-toggle" data-bs-toggle="dropdown">
|
|
<img src="<?php echo getCoinIcon("USDT"); ?>" width="24" height="24" class="me-2 rounded-circle" id="from-coin-img" alt="USDT">
|
|
<span class="fw-bold text-white small" id="from-coin-symbol" data-symbol="USDT">USDT</span>
|
|
</div>
|
|
<ul class="dropdown-menu dropdown-menu-dark bg-dark border-secondary rounded-4 shadow-lg p-2" style="max-height: 300px; overflow-y: auto;">
|
|
<?php
|
|
$swap_coins = ["BTC", "ETH", "USDT", "BNB", "SOL", "XRP", "ADA", "DOGE", "DOT", "MATIC", "LINK", "SHIB"];
|
|
foreach($swap_coins as $sc): ?>
|
|
<li><a class="dropdown-item rounded-3 py-2 d-flex align-items-center gap-2" href="#" onclick="selectCoin('from', '<?= $sc ?>', '<?= $sc ?>', '<?= getCoinIcon($sc) ?>')">
|
|
<img src="<?= getCoinIcon($sc) ?>" width="20" height="20" alt="<?= $sc ?>"> <?= $sc ?>
|
|
</a></li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Switch Icon -->
|
|
<div class="text-center my-n3 position-relative" style="z-index: 2;">
|
|
<div class="bg-primary border border-4 border-dark rounded-circle d-inline-flex p-2 shadow-lg" style="cursor: pointer; transition: all 0.3s;" onclick="switchCoins()">
|
|
<i class="bi bi-arrow-down-up text-white fs-5"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- To -->
|
|
<div class="p-3 p-md-4 mt-n1 mb-4 rounded-4" style="background: #161a1e; border: 1px solid #2b3139;">
|
|
<div class="d-flex justify-content-between mb-3">
|
|
<span class="text-white opacity-50 small fw-bold text-uppercase" style="letter-spacing: 1px;"><?= __("to") ?></span>
|
|
<span class="text-white opacity-50 small"><?= __("balance") ?>: <span class="text-white fw-bold" id="to-balance-val">0.00</span></span>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<input type="number" id="to-amount" class="form-control bg-transparent border-0 text-white p-0 shadow-none w-50 fw-bold" placeholder="0.00" readonly style="color: #fff !important; opacity: 1; font-size: clamp(24px, 5vw, 32px) !important;">
|
|
<div class="ms-auto dropdown">
|
|
<div class="d-flex align-items-center bg-dark p-2 rounded-pill px-2 px-md-3 border border-secondary cursor-pointer shadow-sm dropdown-toggle" data-bs-toggle="dropdown">
|
|
<img src="<?php echo getCoinIcon("BTC"); ?>" width="24" height="24" class="me-2 rounded-circle" id="to-coin-img" alt="BTC">
|
|
<span class="fw-bold text-white small" id="to-coin-symbol" data-symbol="BTC">BTC</span>
|
|
</div>
|
|
<ul class="dropdown-menu dropdown-menu-dark bg-dark border-secondary rounded-4 shadow-lg p-2" style="max-height: 300px; overflow-y: auto;">
|
|
<?php foreach($swap_coins as $sc): ?>
|
|
<li><a class="dropdown-item rounded-3 py-2 d-flex align-items-center gap-2" href="#" onclick="selectCoin('to', '<?= $sc ?>', '<?= $sc ?>', '<?= getCoinIcon($sc) ?>')">
|
|
<img src="<?= getCoinIcon($sc) ?>" width="20" height="20" alt="<?= $sc ?>"> <?= $sc ?>
|
|
</a></li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4 small px-3 py-3 rounded-4" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05);">
|
|
<div class="d-flex justify-content-between text-white opacity-50 mb-2">
|
|
<span class="fw-medium"><?= __("rate") ?></span>
|
|
<span id="swap-rate" class="text-white fw-bold">--</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between text-white opacity-50 mb-2">
|
|
<span class="fw-medium"><?= __("price_impact") ?></span>
|
|
<span class="text-success fw-bold">< 0.01%</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between text-white opacity-50">
|
|
<span class="fw-medium"><?= __("slippage") ?></span>
|
|
<span class="text-white fw-bold">0.5%</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="swap-btn" onclick="executeSwap()" class="btn btn-primary w-100 py-3 rounded-pill fw-bold fs-5 shadow-primary transition-all active-scale" style="background: linear-gradient(135deg, #0062ff, #00d2ff); border: none;"><?= __("swap_now") ?></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const fromInput = document.getElementById("from-amount");
|
|
const toInput = document.getElementById("to-amount");
|
|
const rateEl = document.getElementById("swap-rate");
|
|
const swapBtn = document.getElementById("swap-btn");
|
|
const approxLabel = "<?= __("approx") ?>";
|
|
const userBalances = <?= json_encode($balances) ?>;
|
|
|
|
let fromPrice = 1;
|
|
let toPrice = 1;
|
|
|
|
async function fetchPrice(symbol) {
|
|
if (symbol === "USDT") return 1;
|
|
|
|
// Fallback chain for frontend price fetching
|
|
try {
|
|
// Try CoinCap first as it's CORS-friendly
|
|
const resCC = await fetch(`https://api.coincap.io/v2/assets/${symbol.toLowerCase()}`);
|
|
if (resCC.ok) {
|
|
const dataCC = await resCC.json();
|
|
if (dataCC.data && dataCC.data.priceUsd) return parseFloat(dataCC.data.priceUsd);
|
|
}
|
|
} catch (e) { console.log("CoinCap error, trying OKX..."); }
|
|
|
|
try {
|
|
// Try OKX (might have CORS issues on some browsers)
|
|
const response = await fetch(`https://www.okx.com/api/v5/market/ticker?instId=${symbol}-USDT`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.data && data.data[0]) return parseFloat(data.data[0].last);
|
|
}
|
|
} catch (e) {
|
|
console.error("Price fetch error:", e);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
async function updateRates() {
|
|
const fromSymbol = document.getElementById("from-coin-symbol").dataset.symbol;
|
|
const toSymbol = document.getElementById("to-coin-symbol").dataset.symbol;
|
|
|
|
fromPrice = await fetchPrice(fromSymbol);
|
|
toPrice = await fetchPrice(toSymbol);
|
|
|
|
updateCalculation();
|
|
}
|
|
|
|
function selectCoin(type, symbol, displayName, icon) {
|
|
document.getElementById(type + "-coin-symbol").innerText = displayName;
|
|
document.getElementById(type + "-coin-symbol").dataset.symbol = symbol;
|
|
document.getElementById(type + "-coin-img").src = icon;
|
|
|
|
const bal = userBalances[symbol] || 0;
|
|
document.getElementById(type + "-balance-val").innerText = bal.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 4});
|
|
|
|
updateRates();
|
|
}
|
|
|
|
function switchCoins() {
|
|
const fromSymbol = document.getElementById("from-coin-symbol").dataset.symbol;
|
|
const fromName = document.getElementById("from-coin-symbol").innerText;
|
|
const fromIcon = document.getElementById("from-coin-img").src;
|
|
|
|
const toSymbol = document.getElementById("to-coin-symbol").dataset.symbol;
|
|
const toName = document.getElementById("to-coin-symbol").innerText;
|
|
const toIcon = document.getElementById("to-coin-img").src;
|
|
|
|
selectCoin("from", toSymbol, toName, toIcon);
|
|
selectCoin("to", fromSymbol, fromName, fromIcon);
|
|
}
|
|
|
|
function updateCalculation() {
|
|
const val = parseFloat(fromInput.value) || 0;
|
|
const rate = fromPrice / toPrice;
|
|
toInput.value = (val * rate).toFixed(8);
|
|
|
|
const fromName = document.getElementById("from-coin-symbol").innerText;
|
|
const toName = document.getElementById("to-coin-symbol").innerText;
|
|
|
|
if (fromPrice && toPrice) {
|
|
rateEl.innerText = `1 ${fromName} ${approxLabel} ${(rate).toFixed(8)} ${toName}`;
|
|
} else {
|
|
rateEl.innerText = "--";
|
|
}
|
|
}
|
|
|
|
async function executeSwap() {
|
|
const amount = parseFloat(fromInput.value);
|
|
const fromCoin = document.getElementById("from-coin-symbol").dataset.symbol;
|
|
const toCoin = document.getElementById("to-coin-symbol").dataset.symbol;
|
|
|
|
if (!amount || amount <= 0) {
|
|
Swal.fire({
|
|
icon: 'warning',
|
|
title: '<?= __("enter_amount") ?>',
|
|
background: '#161a1e',
|
|
color: '#fff',
|
|
confirmButtonText: '<?= __("confirm") ?>',
|
|
confirmButtonColor: '#0062ff',
|
|
customClass: {
|
|
popup: 'rounded-4 border border-secondary shadow-lg'
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (amount > (userBalances[fromCoin] || 0)) {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: '<?= __("insufficient_balance") ?>',
|
|
background: '#161a1e',
|
|
color: '#fff',
|
|
confirmButtonText: '<?= __("confirm") ?>',
|
|
confirmButtonColor: '#0062ff',
|
|
customClass: {
|
|
popup: 'rounded-4 border border-secondary shadow-lg'
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Confirmation Modal
|
|
const confirmResult = await Swal.fire({
|
|
title: '<?= __("confirm_swap") ?>',
|
|
html: `<div class="p-3 text-start">
|
|
<div class="mb-2 d-flex justify-content-between"><span><?= __("from") ?>:</span> <span class="fw-bold text-primary">${amount} ${fromCoin}</span></div>
|
|
<div class="mb-0 d-flex justify-content-between"><span><?= __("to") ?>:</span> <span class="fw-bold text-success">${toInput.value} ${toCoin}</span></div>
|
|
</div>`,
|
|
icon: 'question',
|
|
showCancelButton: true,
|
|
confirmButtonText: '<?= __("confirm") ?>',
|
|
cancelButtonText: '<?= __("cancel") ?>',
|
|
background: '#161a1e',
|
|
color: '#fff',
|
|
confirmButtonColor: '#0062ff',
|
|
cancelButtonColor: '#2b3139',
|
|
customClass: {
|
|
popup: 'rounded-4 border border-secondary shadow-lg',
|
|
confirmButton: 'rounded-pill px-4',
|
|
cancelButton: 'rounded-pill px-4'
|
|
}
|
|
});
|
|
|
|
if (!confirmResult.isConfirmed) return;
|
|
|
|
Swal.fire({
|
|
title: '<?= __("processing") ?>',
|
|
html: '<?= __("swap_processing_desc") ?>',
|
|
allowOutsideClick: false,
|
|
background: '#161a1e',
|
|
color: '#fff',
|
|
didOpen: () => {
|
|
Swal.showLoading();
|
|
},
|
|
customClass: {
|
|
popup: 'rounded-4 border border-secondary shadow-lg'
|
|
}
|
|
});
|
|
|
|
swapBtn.disabled = true;
|
|
const originalText = swapBtn.innerText;
|
|
swapBtn.innerText = '<?= __("processing") ?>';
|
|
|
|
const formData = new FormData();
|
|
formData.append("from_coin", fromCoin);
|
|
formData.append("to_coin", toCoin);
|
|
formData.append("amount", amount);
|
|
|
|
try {
|
|
const response = await fetch("api/swap.php", {
|
|
method: "POST",
|
|
body: formData
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
await Swal.fire({
|
|
icon: 'success',
|
|
title: '<?= __("success") ?>',
|
|
text: '<?= __("swap_success_desc") ?>',
|
|
background: '#161a1e',
|
|
color: '#fff',
|
|
confirmButtonText: '<?= __("confirm") ?>',
|
|
confirmButtonColor: '#0062ff',
|
|
customClass: {
|
|
popup: 'rounded-4 border border-secondary shadow-lg'
|
|
}
|
|
});
|
|
location.reload();
|
|
} else {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: '<?= __("failed") ?>',
|
|
text: result.error || '<?= __("unknown_error") ?>',
|
|
background: '#161a1e',
|
|
color: '#fff',
|
|
confirmButtonText: '<?= __("confirm") ?>',
|
|
confirmButtonColor: '#0062ff',
|
|
customClass: {
|
|
popup: 'rounded-4 border border-secondary shadow-lg'
|
|
}
|
|
});
|
|
swapBtn.disabled = false;
|
|
swapBtn.innerText = originalText;
|
|
}
|
|
} catch (e) {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: '<?= __("request_failed") ?>',
|
|
background: '#161a1e',
|
|
color: '#fff',
|
|
confirmButtonText: '<?= __("confirm") ?>',
|
|
confirmButtonColor: '#0062ff',
|
|
customClass: {
|
|
popup: 'rounded-4 border border-secondary shadow-lg'
|
|
}
|
|
});
|
|
swapBtn.disabled = false;
|
|
swapBtn.innerText = originalText;
|
|
}
|
|
}
|
|
|
|
fromInput.addEventListener("input", updateCalculation);
|
|
|
|
// Initial setup
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
selectCoin("from", "USDT", "USDT", "<?php echo getCoinIcon("USDT"); ?>");
|
|
selectCoin("to", "BTC", "BTC", "<?php echo getCoinIcon("BTC"); ?>");
|
|
});
|
|
|
|
// Animate switch icon
|
|
document.querySelector(".bg-primary.rounded-circle").addEventListener("click", function() {
|
|
this.style.transform = this.style.transform === "rotate(180deg)" ? "rotate(0deg)" : "rotate(180deg)";
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.shadow-primary {
|
|
box-shadow: 0 10px 25px rgba(0, 98, 255, 0.4);
|
|
}
|
|
.cursor-pointer { cursor: pointer; }
|
|
#swap-rate { font-family: "Roboto Mono", monospace; }
|
|
.active-scale:active { transform: scale(0.98); }
|
|
|
|
/* Beautiful SweetAlert2 Customization */
|
|
.swal2-popup.rounded-4 {
|
|
border-radius: 24px !important;
|
|
padding: 2rem !important;
|
|
background: #161a1e !important;
|
|
}
|
|
.swal2-title {
|
|
font-size: 1.5rem !important;
|
|
font-weight: 700 !important;
|
|
}
|
|
.swal2-confirm.rounded-pill, .swal2-cancel.rounded-pill {
|
|
padding: 0.8rem 2rem !important;
|
|
font-size: 0.9rem !important;
|
|
font-weight: 700 !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.swal2-icon {
|
|
border-width: 2px !important;
|
|
}
|
|
</style>
|
|
<?php require_once __DIR__ . "/includes/footer.php"; ?>
|