682 lines
19 KiB
HTML
682 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fa" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>🪙 داشبورد زنده قیمت سکه — اندیشکده فولاد آینده</title>
|
||
|
||
<!-- Modern Persian & English Typography -->
|
||
<link href="https://cdn.jsdelivr.net/gh/rastikerdar/vazir-font@v30.1.0/dist/font-face.css" rel="stylesheet" type="text/css" />
|
||
|
||
<style>
|
||
:root {
|
||
--bg-gradient: linear-gradient(135deg, #0b0f19 0%, #111827 100%);
|
||
--card-bg: rgba(17, 24, 39, 0.7);
|
||
--card-border: rgba(255, 255, 255, 0.08);
|
||
--text-primary: #f3f4f6;
|
||
--text-muted: #9ca3af;
|
||
--accent-gold: #f59e0b;
|
||
--accent-gold-glow: rgba(245, 158, 11, 0.15);
|
||
|
||
--color-up: #10b981;
|
||
--color-up-glow: rgba(16, 185, 129, 0.2);
|
||
--color-down: #ef4444;
|
||
--color-down-glow: rgba(239, 68, 68, 0.2);
|
||
--color-neutral: #6b7280;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: 'Vazir', 'Inter', -apple-system, sans-serif;
|
||
}
|
||
|
||
body {
|
||
background: var(--bg-gradient);
|
||
color: var(--text-primary);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-x: hidden;
|
||
padding: 2rem 1.5rem;
|
||
}
|
||
|
||
/* Container */
|
||
.dashboard-container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
flex-grow: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2rem;
|
||
}
|
||
|
||
/* Header */
|
||
header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 1.5rem;
|
||
border-bottom: 1px solid var(--card-border);
|
||
padding-bottom: 1.5rem;
|
||
}
|
||
|
||
.brand-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.brand-logo {
|
||
font-size: 2.5rem;
|
||
animation: float 4s ease-in-out infinite;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.8rem;
|
||
font-weight: 800;
|
||
background: linear-gradient(to left, #ffffff, #f59e0b);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
margin-bottom: 0.2rem;
|
||
}
|
||
|
||
.brand-sub {
|
||
font-size: 0.85rem;
|
||
color: var(--text-muted);
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
/* Status Pill and Countdown */
|
||
.status-panel {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1.2rem;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
padding: 0.6rem 1.2rem;
|
||
border-radius: 50px;
|
||
border: 1px solid var(--card-border);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.status-pill {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.85rem;
|
||
font-weight: bold;
|
||
color: var(--color-up);
|
||
}
|
||
|
||
.pulse-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
background: var(--color-up);
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 0 0 var(--color-up-glow);
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
|
||
.timer-section {
|
||
font-size: 0.85rem;
|
||
color: var(--text-muted);
|
||
border-right: 1px solid var(--card-border);
|
||
padding-right: 1.2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.timer-val {
|
||
font-weight: 900;
|
||
color: var(--accent-gold);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* main price grid */
|
||
.price-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
/* Card styling - Glassmorphism */
|
||
.coin-card {
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--card-border);
|
||
border-radius: 20px;
|
||
padding: 1.8rem 1.5rem;
|
||
backdrop-filter: blur(12px);
|
||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.2rem;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.3s, box-shadow 0.3s;
|
||
}
|
||
|
||
.coin-card:hover {
|
||
transform: translateY(-5px);
|
||
border-color: rgba(245, 158, 11, 0.3);
|
||
box-shadow: 0 12px 40px 0 rgba(245, 158, 11, 0.08);
|
||
}
|
||
|
||
/* Price Flashes */
|
||
@keyframes up-flash {
|
||
0% { border-color: var(--color-up); box-shadow: 0 0 25px var(--color-up-glow); }
|
||
100% { border-color: var(--card-border); box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); }
|
||
}
|
||
|
||
@keyframes down-flash {
|
||
0% { border-color: var(--color-down); box-shadow: 0 0 25px var(--color-down-glow); }
|
||
100% { border-color: var(--card-border); box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); }
|
||
}
|
||
|
||
.up-flash {
|
||
animation: up-flash 1.5s ease-out;
|
||
}
|
||
|
||
.down-flash {
|
||
animation: down-flash 1.5s ease-out;
|
||
}
|
||
|
||
/* Card Header */
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.coin-title {
|
||
font-size: 1.1rem;
|
||
font-weight: 800;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.coin-badge {
|
||
font-size: 1.5rem;
|
||
background: var(--accent-gold-glow);
|
||
padding: 0.4rem;
|
||
border-radius: 12px;
|
||
color: var(--accent-gold);
|
||
border: 1px solid rgba(245, 158, 11, 0.2);
|
||
}
|
||
|
||
/* Card Price Body */
|
||
.price-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.3rem;
|
||
}
|
||
|
||
.price-toman {
|
||
font-size: 1.8rem;
|
||
font-weight: 900;
|
||
color: #ffffff;
|
||
letter-spacing: -0.5px;
|
||
}
|
||
|
||
.price-toman span {
|
||
font-size: 0.85rem;
|
||
font-weight: normal;
|
||
color: var(--text-muted);
|
||
margin-right: 0.3rem;
|
||
}
|
||
|
||
.price-rial {
|
||
font-size: 0.8rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* Change badge indicators */
|
||
.change-badge {
|
||
align-self: flex-start;
|
||
font-size: 0.78rem;
|
||
font-weight: bold;
|
||
padding: 0.3rem 0.7rem;
|
||
border-radius: 50px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.3rem;
|
||
direction: ltr; /* English layout inside pill */
|
||
}
|
||
|
||
.change-up {
|
||
background: rgba(16, 185, 129, 0.1);
|
||
color: var(--color-up);
|
||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||
}
|
||
|
||
.change-down {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
color: var(--color-down);
|
||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||
}
|
||
|
||
.change-neutral {
|
||
background: rgba(107, 114, 128, 0.1);
|
||
color: var(--color-neutral);
|
||
border: 1px solid rgba(107, 114, 128, 0.2);
|
||
}
|
||
|
||
/* Card Footer Info */
|
||
.card-footer {
|
||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||
padding-top: 0.8rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.4rem;
|
||
font-size: 0.78rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.info-val {
|
||
font-weight: bold;
|
||
color: #ffffff;
|
||
}
|
||
|
||
/* History table area */
|
||
.history-card {
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--card-border);
|
||
border-radius: 20px;
|
||
padding: 1.8rem;
|
||
backdrop-filter: blur(12px);
|
||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.2rem;
|
||
}
|
||
|
||
.history-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.history-title {
|
||
font-size: 1.2rem;
|
||
font-weight: 800;
|
||
color: #ffffff;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.table-container {
|
||
overflow-x: auto;
|
||
border-radius: 12px;
|
||
border: 1px solid var(--card-border);
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
text-align: right;
|
||
font-size: 0.88rem;
|
||
}
|
||
|
||
th {
|
||
background: rgba(255, 255, 255, 0.02);
|
||
color: var(--text-muted);
|
||
font-weight: bold;
|
||
padding: 1rem;
|
||
border-bottom: 1px solid var(--card-border);
|
||
}
|
||
|
||
td {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||
color: #e5e7eb;
|
||
}
|
||
|
||
tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
tr:hover td {
|
||
background: rgba(255, 255, 255, 0.01);
|
||
}
|
||
|
||
/* Animations */
|
||
@keyframes pulse {
|
||
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
|
||
70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
|
||
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
||
}
|
||
|
||
@keyframes float {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-8px); }
|
||
}
|
||
|
||
footer {
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
font-size: 0.8rem;
|
||
margin-top: 3rem;
|
||
border-top: 1px solid var(--card-border);
|
||
padding-top: 1.5rem;
|
||
}
|
||
|
||
/* Responsive adjustments */
|
||
@media (max-width: 768px) {
|
||
body {
|
||
padding: 1rem;
|
||
}
|
||
header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
.status-panel {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="dashboard-container">
|
||
|
||
<!-- Brand Header -->
|
||
<header>
|
||
<div class="brand-section">
|
||
<div class="brand-logo">🪙</div>
|
||
<div>
|
||
<h1>داشبورد زنده قیمت سکه</h1>
|
||
<div class="brand-sub">اندیشکده فولاد آینده — مرکز تحلیل راهبردی بازار طلا و صنعت</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Real-time updates panel -->
|
||
<div class="status-panel">
|
||
<div class="status-pill">
|
||
<div class="pulse-dot"></div>
|
||
<span>بروزرسانی زنده فعال</span>
|
||
</div>
|
||
<div class="timer-section">
|
||
<span>داده جدید در: </span>
|
||
<span class="timer-val" id="countdown-val">۶۰ ثانیه</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 5 Live Coin Cards -->
|
||
<section class="price-grid" id="cards-grid">
|
||
<!-- Cards will be dynamically rendered here -->
|
||
</section>
|
||
|
||
<!-- Recent History Log Table -->
|
||
<section class="history-card">
|
||
<div class="history-header">
|
||
<h2 class="history-title">⏳ تاریخچه نوسانات اخیر (ثبت خودکار هر ۱ دقیقه)</h2>
|
||
</div>
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>زمان ثبت سیستم</th>
|
||
<th>نام سکه</th>
|
||
<th>قیمت فعلی (تومان)</th>
|
||
<th>میزان و درصد تغییر</th>
|
||
<th>کمترین قیمت روز</th>
|
||
<th>بیشترین قیمت روز</th>
|
||
<th>زمان بروزرسانی سایت</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="history-tbody">
|
||
<tr>
|
||
<td colspan="7" style="text-align: center; color: var(--text-muted); padding: 2rem;">در حال لود اطلاعات از سرور...</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<footer>
|
||
مرکز مستقل تحلیل راهبردی و سیاستگذاری بازار و صنایع — تمامی نرخها به صورت مستقیم و بدون کش از بازار تهران دریافت میشوند.
|
||
</footer>
|
||
|
||
</div>
|
||
|
||
<!-- Real-time Dynamic script re-loader and state manager -->
|
||
<script>
|
||
let lastFetchedTime = null;
|
||
let priceHistory = [];
|
||
let previousPrices = {};
|
||
let countdownSecs = 60;
|
||
|
||
// Format numbers helper
|
||
function fmtNum(val) {
|
||
if (typeof val === 'string') {
|
||
val = parseInt(val.replace(/[^\d]/g, ''));
|
||
}
|
||
return val ? val.toLocaleString('fa-IR') : '۰';
|
||
}
|
||
|
||
// Convert numbers to Persian characters
|
||
const persianDigits = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
|
||
function toPersianDigits(str) {
|
||
if (str === null || str === undefined) return '';
|
||
return str.toString().replace(/[0-9]/g, w => persianDigits[+w]);
|
||
}
|
||
|
||
// Load data from live_prices.js dynamically to bypass CORS file:// issues
|
||
function reloadPriceScript() {
|
||
// Remove any previously injected script
|
||
const oldScript = document.getElementById('live-data-script');
|
||
if (oldScript) {
|
||
oldScript.remove();
|
||
}
|
||
|
||
// Create a fresh script element with cache buster
|
||
const script = document.createElement('script');
|
||
script.id = 'live-data-script';
|
||
script.src = 'live_prices.js?_t=' + Date.now();
|
||
|
||
script.onload = function() {
|
||
if (window.LIVE_COIN_PRICES) {
|
||
updateUI(window.LIVE_COIN_PRICES);
|
||
}
|
||
};
|
||
|
||
document.body.appendChild(script);
|
||
}
|
||
|
||
function updateUI(data) {
|
||
// If no new data fetched since last load, do nothing
|
||
if (lastFetchedTime === data.last_fetch) {
|
||
return;
|
||
}
|
||
|
||
lastFetchedTime = data.last_fetch;
|
||
countdownSecs = 60; // Reset countdown on fresh fetch
|
||
|
||
const grid = document.getElementById('cards-grid');
|
||
grid.innerHTML = '';
|
||
|
||
// Target emojis
|
||
const coinEmojis = {
|
||
"سکه امامی": "👑",
|
||
"سکه بهار آزادی": "🕌",
|
||
"نیم سکه": "🌓",
|
||
"ربع سکه": "🌗",
|
||
"سکه گرمی": "⚡"
|
||
};
|
||
|
||
data.prices.forEach(coin => {
|
||
// Compare price with previous to determine change flashes
|
||
const oldPrice = previousPrices[coin.name];
|
||
let flashClass = '';
|
||
if (oldPrice !== undefined) {
|
||
if (coin.price_toman > oldPrice) flashClass = 'up-flash';
|
||
else if (coin.price_toman < oldPrice) flashClass = 'down-flash';
|
||
}
|
||
previousPrices[coin.name] = coin.price_toman;
|
||
|
||
// Parse change percentage and sign
|
||
const isUp = coin.change.includes('+') || (!coin.change.includes('-') && !coin.change.startsWith('0') && !coin.change.includes('(0%'));
|
||
const isZero = coin.change.startsWith('0') || coin.change.includes('(0%') || coin.change === '0';
|
||
|
||
let changeClass = 'change-neutral';
|
||
let changeIcon = '●';
|
||
if (!isZero) {
|
||
if (isUp) {
|
||
changeClass = 'change-up';
|
||
changeIcon = '▲';
|
||
} else {
|
||
changeClass = 'change-down';
|
||
changeIcon = '▼';
|
||
}
|
||
}
|
||
|
||
// Render card html
|
||
const emoji = coinEmojis[coin.name] || "🪙";
|
||
const cardHtml = `
|
||
<div class="coin-card ${flashClass}">
|
||
<div class="card-header">
|
||
<span class="coin-title">${coin.name}</span>
|
||
<span class="coin-badge">${emoji}</span>
|
||
</div>
|
||
|
||
<div class="price-section">
|
||
<div class="price-toman">
|
||
${fmtNum(coin.price_toman)} <span>تومان</span>
|
||
</div>
|
||
<div class="price-rial">
|
||
${fmtNum(coin.price_rial)} ریال
|
||
</div>
|
||
</div>
|
||
|
||
<div class="change-badge ${changeClass}">
|
||
<span>${toPersianDigits(coin.change)}</span>
|
||
<span>${changeIcon}</span>
|
||
</div>
|
||
|
||
<div class="card-footer">
|
||
<div class="info-row">
|
||
<span>کمترین قیمت روز:</span>
|
||
<span class="info-val">${fmtNum(coin.min_toman)} ت</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span>بیشترین قیمت روز:</span>
|
||
<span class="info-val">${fmtNum(coin.max_toman)} ت</span>
|
||
</div>
|
||
<div class="info-row" style="margin-top: 0.4rem; border-top: 1px dashed rgba(255,255,255,0.03); padding-top: 0.4rem;">
|
||
<span>بروزرسانی سایت:</span>
|
||
<span class="info-val" style="color: var(--accent-gold); font-weight: bold;">${toPersianDigits(coin.last_update)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
grid.insertAdjacentHTML('beforeend', cardHtml);
|
||
|
||
// Push record to local rolling history
|
||
pushToRollingHistory(data.last_fetch, coin);
|
||
});
|
||
|
||
// Update history table view
|
||
renderHistoryTable();
|
||
}
|
||
|
||
// Keep the last 15 ticks in local storage rolling history
|
||
function pushToRollingHistory(fetchTime, coin) {
|
||
// Avoid duplicate logs for the exact same fetch time
|
||
const duplicate = priceHistory.some(h => h.time === fetchTime && h.name === coin.name);
|
||
if (duplicate) return;
|
||
|
||
priceHistory.push({
|
||
time: fetchTime,
|
||
name: coin.name,
|
||
price_toman: coin.price_toman,
|
||
change: coin.change,
|
||
min_toman: coin.min_toman,
|
||
max_toman: coin.max_toman,
|
||
last_update: coin.last_update
|
||
});
|
||
|
||
// Limit size to last 50 entries
|
||
if (priceHistory.length > 50) {
|
||
priceHistory.shift();
|
||
}
|
||
|
||
localStorage.setItem('coin_price_history_v2', JSON.stringify(priceHistory));
|
||
}
|
||
|
||
function renderHistoryTable() {
|
||
const tbody = document.getElementById('history-tbody');
|
||
tbody.innerHTML = '';
|
||
|
||
// Display logs in reverse order (newest first)
|
||
const reversedLogs = [...priceHistory].reverse();
|
||
|
||
if (reversedLogs.length === 0) {
|
||
tbody.innerHTML = `<tr><td colspan="7" style="text-align: center; color: var(--text-muted);">هیچ رکوردی در تاریخچه ثبت نشده است.</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
reversedLogs.forEach(row => {
|
||
const isUp = row.change.includes('+') || (!row.change.includes('-') && !row.change.startsWith('0') && !row.change.includes('(0%'));
|
||
const isZero = row.change.startsWith('0') || row.change.includes('(0%') || row.change === '0';
|
||
|
||
let color = 'var(--text-primary)';
|
||
if (!isZero) {
|
||
color = isUp ? 'var(--color-up)' : 'var(--color-down)';
|
||
}
|
||
|
||
const trHtml = `
|
||
<tr>
|
||
<td style="font-weight: bold; color: var(--accent-gold);">${toPersianDigits(row.time)}</td>
|
||
<td style="font-weight: bold;">${row.name}</td>
|
||
<td style="font-weight: 900;">${fmtNum(row.price_toman)} تومان</td>
|
||
<td style="color: ${color}; font-weight: bold; direction: ltr;">${toPersianDigits(row.change)}</td>
|
||
<td>${fmtNum(row.min_toman)} تومان</td>
|
||
<td>${fmtNum(row.max_toman)} تومان</td>
|
||
<td style="color: var(--text-muted);">${toPersianDigits(row.last_update)}</td>
|
||
</tr>
|
||
`;
|
||
tbody.insertAdjacentHTML('beforeend', trHtml);
|
||
});
|
||
}
|
||
|
||
// Initialize state
|
||
if (localStorage.getItem('coin_price_history_v2')) {
|
||
try {
|
||
priceHistory = JSON.parse(localStorage.getItem('coin_price_history_v2'));
|
||
} catch(e) {
|
||
priceHistory = [];
|
||
}
|
||
}
|
||
|
||
// Loop timer
|
||
setInterval(() => {
|
||
if (countdownSecs > 1) {
|
||
countdownSecs--;
|
||
document.getElementById('countdown-val').innerText = toPersianDigits(countdownSecs) + ' ثانیه';
|
||
} else {
|
||
document.getElementById('countdown-val').innerText = 'در حال دریافت...';
|
||
}
|
||
}, 1000);
|
||
|
||
// Initial loading
|
||
reloadPriceScript();
|
||
|
||
// Poll the JS file every 2 seconds for ultra-responsive updates
|
||
setInterval(reloadPriceScript, 2000);
|
||
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|