feat(svelte): migrate to svelte

This commit is contained in:
tiennm99 committed 2025-06-26 23:22:28 +07:00
1 parent da3ebb5a45
commit e8d82e0120
8 files changed
+546 -791

No files matched your search

-266
View File
@@ -1,266 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exchange Rate Export</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
background: #f7f7f7;
}
.container {
background: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
max-width: 700px;
margin: auto;
}
h1 {
text-align: center;
}
label {
margin-right: 8px;
}
input[type="date"] {
margin-right: 16px;
padding: 4px 8px;
}
button {
padding: 8px 16px;
margin-right: 8px;
border: none;
border-radius: 4px;
background: #007bff;
color: #fff;
cursor: pointer;
font-size: 1rem;
}
button:disabled {
background: #aaa;
cursor: not-allowed;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 24px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}
th {
background: #007bff;
color: #fff;
}
.loading {
text-align: center;
margin-top: 20px;
color: #007bff;
}
.error {
color: red;
text-align: center;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Exchange Rate Export (USD)</h1>
<div style="margin-bottom: 20px;">
<label for="start-date">Start Date:</label>
<input type="date" id="start-date">
<label for="end-date">End Date:</label>
<input type="date" id="end-date">
<button id="fetch-btn">Fetch</button>
<button id="export-btn" disabled>Export CSV</button>
</div>
<div id="status"></div>
<table id="result-table" style="display:none;">
<thead>
<tr>
<th>Date</th>
<th>nameVI</th>
<th>muaTm</th>
<th>muaCk</th>
<th>currency</th>
<th>nameEN</th>
<th>ban</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<script>
const fetchBtn = document.getElementById('fetch-btn');
const exportBtn = document.getElementById('export-btn');
const startDateInput = document.getElementById('start-date');
const endDateInput = document.getElementById('end-date');
const statusDiv = document.getElementById('status');
const table = document.getElementById('result-table');
const tbody = table.querySelector('tbody');
let results = [];
function formatDate(date) {
// Format as dd/MM/yyyy
const d = new Date(date);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}/${month}/${year}`;
}
function getDateRange(start, end) {
const dates = [];
let current = new Date(start);
const last = new Date(end);
while (current <= last) {
dates.push(new Date(current));
current.setDate(current.getDate() + 1);
}
return dates;
}
async function fetchNamerecord(dateStr) {
const url = 'https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet';
const payload = `date=${encodeURIComponent(dateStr)}`;
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: payload
});
if (!res.ok) throw new Error('API 1 error');
const data = await res.json();
if (data.status !== 1 || !data.data || !data.data.length) return null;
// Get the latest (max time) record
let latest = data.data[0];
for (const rec of data.data) {
if (rec.time > latest.time) latest = rec;
}
return latest.namerecord;
} catch (e) {
return null;
}
}
async function fetchUSD(dateStr, namerecord) {
const url = 'https://bidv.com.vn/ServicesBIDV/ExchangeDetailServlet';
const payload = `date=${encodeURIComponent(dateStr)}&time=${namerecord}`;
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: payload
});
if (!res.ok) throw new Error('API 2 error');
const data = await res.json();
if (data.status !== 1 || !data.data) return null;
// Find USD (currency === 'USD')
const usd = data.data.find(item => item.currency === 'USD');
if (!usd) return null;
return {
nameVI: usd.nameVI,
muaTm: usd.muaTm,
muaCk: usd.muaCk,
currency: usd.currency,
nameEN: usd.nameEN,
ban: usd.ban
};
} catch (e) {
return null;
}
}
function renderTable(data) {
tbody.innerHTML = '';
for (const row of data) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${row.date}</td>
<td>${row.nameVI}</td>
<td>${row.muaTm}</td>
<td>${row.muaCk}</td>
<td>${row.currency}</td>
<td>${row.nameEN}</td>
<td>${row.ban}</td>
`;
tbody.appendChild(tr);
}
table.style.display = data.length ? '' : 'none';
}
function exportCSV(data) {
const header = ['Date', 'nameVI', 'muaTm', 'muaCk', 'currency', 'nameEN', 'ban'];
const rows = [header];
for (const row of data) {
rows.push([
row.date,
row.nameVI,
row.muaTm,
row.muaCk,
row.currency,
row.nameEN,
row.ban
]);
}
const csvContent = rows.map(r => r.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(',')).join('\r\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'usd_exchange_rates.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
fetchBtn.onclick = async () => {
const start = startDateInput.value;
const end = endDateInput.value;
if (!start || !end) {
statusDiv.innerHTML = '<div class="error">Please select both start and end dates.</div>';
return;
}
if (start > end) {
statusDiv.innerHTML = '<div class="error">Start date must be before or equal to end date.</div>';
return;
}
statusDiv.innerHTML = '<div class="loading">Fetching data, please wait...</div>';
fetchBtn.disabled = true;
exportBtn.disabled = true;
results = [];
renderTable([]);
const dates = getDateRange(start, end);
for (let i = 0; i < dates.length; i++) {
const dateObj = dates[i];
const dateStr = formatDate(dateObj);
statusDiv.innerHTML = `<div class=\"loading\">Fetching: ${dateStr} (${i+1}/${dates.length})</div>`;
const namerecord = await fetchNamerecord(dateStr);
if (!namerecord) continue;
const usd = await fetchUSD(dateStr, namerecord);
if (usd) {
results.push({ date: dateStr, ...usd });
renderTable(results);
}
}
statusDiv.innerHTML = results.length ? '' : '<div class="error">No data found for the selected range.</div>';
fetchBtn.disabled = false;
exportBtn.disabled = results.length === 0;
};
exportBtn.onclick = () => {
if (results.length) exportCSV(results);
};
</script>
</body>
</html>
-279
View File
@@ -1,279 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exchange Rate Export</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
background: #f7f7f7;
}
.container {
background: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
max-width: 700px;
margin: auto;
}
h1 {
text-align: center;
}
label {
margin-right: 8px;
}
input[type="date"] {
margin-right: 16px;
padding: 4px 8px;
}
button {
padding: 8px 16px;
margin-right: 8px;
border: none;
border-radius: 4px;
background: #007bff;
color: #fff;
cursor: pointer;
font-size: 1rem;
}
button:disabled {
background: #aaa;
cursor: not-allowed;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 24px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}
th {
background: #007bff;
color: #fff;
}
.loading {
text-align: center;
margin-top: 20px;
color: #007bff;
}
.error {
color: red;
text-align: center;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Exchange Rate Export (USD)</h1>
<div style="margin-bottom: 20px;">
<label for="start-date">Start Date:</label>
<input type="date" id="start-date">
<label for="end-date">End Date:</label>
<input type="date" id="end-date">
<button id="fetch-btn">Fetch</button>
<button id="export-btn" disabled>Export CSV</button>
<label for="bank-select">Bank:</label>
<select id="bank-select">
<option value="bidv">BIDV</option>
<option value="tcb">Techcombank</option>
</select>
</div>
<div id="status"></div>
<table id="result-table" style="display:none;">
<thead>
<tr>
<th>Date</th>
<th>nameVI</th>
<th>muaTm</th>
<th>muaCk</th>
<th>currency</th>
<th>nameEN</th>
<th>ban</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<script>
const fetchBtn = document.getElementById('fetch-btn');
const exportBtn = document.getElementById('export-btn');
const startDateInput = document.getElementById('start-date');
const endDateInput = document.getElementById('end-date');
const statusDiv = document.getElementById('status');
const table = document.getElementById('result-table');
const tbody = table.querySelector('tbody');
let results = [];
function formatDate(date) {
// Format as dd/MM/yyyy
const d = new Date(date);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}/${month}/${year}`;
}
function getDateRange(start, end) {
const dates = [];
let current = new Date(start);
const last = new Date(end);
while (current <= last) {
dates.push(new Date(current));
current.setDate(current.getDate() + 1);
}
return dates;
}
async function fetchNamerecord(dateStr) {
const url = 'https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet';
const payload = `date=${encodeURIComponent(dateStr)}`;
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: payload
});
if (!res.ok) throw new Error('API 1 error');
const data = await res.json();
if (data.status !== 1 || !data.data || !data.data.length) return null;
// Get the latest (max time) record
let latest = data.data[0];
for (const rec of data.data) {
if (rec.time > latest.time) latest = rec;
}
return latest.namerecord;
} catch (e) {
return null;
}
}
async function fetchUSD(dateStr, namerecord) {
const url = 'https://bidv.com.vn/ServicesBIDV/ExchangeDetailServlet';
const payload = `date=${encodeURIComponent(dateStr)}&time=${namerecord}`;
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: payload
});
if (!res.ok) throw new Error('API 2 error');
const data = await res.json();
if (data.status !== 1 || !data.data) return null;
// Find USD (currency === 'USD')
const usd = data.data.find(item => item.currency === 'USD');
if (!usd) return null;
return {
nameVI: usd.nameVI,
muaTm: usd.muaTm,
muaCk: usd.muaCk,
currency: usd.currency,
nameEN: usd.nameEN,
ban: usd.ban
};
} catch (e) {
return null;
}
}
function renderTable(data) {
tbody.innerHTML = '';
for (const row of data) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${row.date}</td>
<td>${row.nameVI}</td>
<td>${row.muaTm}</td>
<td>${row.muaCk}</td>
<td>${row.currency}</td>
<td>${row.nameEN}</td>
<td>${row.ban}</td>
`;
tbody.appendChild(tr);
}
table.style.display = data.length ? '' : 'none';
}
function exportCSV(data) {
const header = ['Date', 'nameVI', 'muaTm', 'muaCk', 'currency', 'nameEN', 'ban'];
const rows = [header];
for (const row of data) {
rows.push([
row.date,
row.nameVI,
row.muaTm,
row.muaCk,
row.currency,
row.nameEN,
row.ban
]);
}
const csvContent = rows.map(r => r.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(',')).join('\r\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'usd_exchange_rates.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
fetchBtn.onclick = async () => {
const start = startDateInput.value;
const end = endDateInput.value;
if (!start || !end) {
statusDiv.innerHTML = '<div class="error">Please select both start and end dates.</div>';
return;
}
if (start > end) {
statusDiv.innerHTML = '<div class="error">Start date must be before or equal to end date.</div>';
return;
}
statusDiv.innerHTML = '<div class="loading">Fetching data, please wait...</div>';
fetchBtn.disabled = true;
exportBtn.disabled = true;
results = [];
renderTable([]);
const dates = getDateRange(start, end);
for (let i = 0; i < dates.length; i++) {
const dateObj = dates[i];
const dateStr = formatDate(dateObj);
statusDiv.innerHTML = `<div class=\"loading\">Fetching: ${dateStr} (${i+1}/${dates.length})</div>`;
const namerecord = await fetchNamerecord(dateStr);
if (!namerecord) continue;
const usd = await fetchUSD(dateStr, namerecord);
if (usd) {
results.push({ date: dateStr, ...usd });
renderTable(results);
}
}
statusDiv.innerHTML = results.length ? '' : '<div class="error">No data found for the selected range.</div>';
fetchBtn.disabled = false;
exportBtn.disabled = results.length === 0;
};
exportBtn.onclick = () => {
if (results.length) exportCSV(results);
};
document.getElementById('bank-select').onchange = function() {
if (this.value === 'bidv') {
window.location.href = 'bidv.html';
} else if (this.value === 'tcb') {
window.location.href = 'tcb.html';
}
};
</script>
</body>
</html>
+41
View File
@@ -7,6 +7,9 @@
"": {
"name": "exchange-rate-export",
"version": "0.0.1",
"dependencies": {
"date-fns": "^4.1.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-cloudflare": "^7.0.4",
@@ -14,6 +17,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"svelte": "^5.0.0",
"vite": "^6.2.6",
"vite-plugin-devtools-json": "^0.2.0",
"wrangler": "^4.22.0"
}
},
@@ -1596,6 +1600,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -2289,6 +2303,20 @@
"ufo": "^1.6.1"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
@@ -2364,6 +2392,19 @@
}
}
},
"node_modules/vite-plugin-devtools-json": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-devtools-json/-/vite-plugin-devtools-json-0.2.0.tgz",
"integrity": "sha512-K7PoaWOEJECZ1n3VbhJXsUAX2PsO0xY7KFMM/Leh7tUev0M5zi+lz+vnVVdCK17IOK9Jp9rdzHXc08cnQirGbg==",
"dev": true,
"license": "MIT",
"dependencies": {
"uuid": "^11.1.0"
},
"peerDependencies": {
"vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
}
},
"node_modules/vitefu": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.7.tgz",
+5 -1
View File
@@ -17,6 +17,10 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"svelte": "^5.0.0",
"vite": "^6.2.6",
"vite-plugin-devtools-json": "^0.2.0",
"wrangler": "^4.22.0"
},
"dependencies": {
"date-fns": "^4.1.0"
}
}
}
+470
View File
@@ -0,0 +1,470 @@
<script>
import { format } from 'date-fns';
// Props
export let defaultStartDate = '';
export let defaultEndDate = '';
// State
let startDate = defaultStartDate;
let endDate = defaultEndDate;
let selectedBank = 'bidv'; // 'bidv' or 'tcb'
let isLoading = false;
let error = '';
let results = [];
let showTable = false;
// Format date for display
const formatDisplayDate = (dateStr) => {
const [year, month, day] = dateStr.split('-');
return `${day}/${month}/${year}`;
};
// Get date range between start and end dates
const getDateRange = (start, end) => {
const dates = [];
let current = new Date(start);
const last = new Date(end);
while (current <= last) {
dates.push(new Date(current));
current.setDate(current.getDate() + 1);
}
return dates;
};
// Format date for API requests
const formatDateForApi = (date) => {
const d = new Date(date);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}/${month}/${year}`;
};
// Fetch BIDV exchange rates
const fetchBIDVRates = async (dateObj) => {
const dateStr = format(dateObj, 'dd/MM/yyyy');
// First API call to get namerecord
const timeRes = await fetch('https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `date=${encodeURIComponent(dateStr)}`
});
if (!timeRes.ok) throw new Error('Failed to fetch BIDV time data');
const timeData = await timeRes.json();
if (timeData.status !== 1 || !timeData.data?.length) return null;
// Get the latest record
const latest = timeData.data.reduce((latest, current) =>
current.time > latest.time ? current : latest, timeData.data[0]);
// Second API call to get exchange rates
const rateRes = await fetch('https://bidv.com.vn/ServicesBIDV/ExchangeDetailServlet', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `date=${encodeURIComponent(dateStr)}&time=${latest.namerecord}`
});
if (!rateRes.ok) throw new Error('Failed to fetch BIDV rate data');
const rateData = await rateRes.json();
if (rateData.status !== 1 || !rateData.data) return null;
// Find USD data
const usd = rateData.data.find(item => item.currency === 'USD');
if (!usd) return null;
return {
date: dateStr,
nameVI: usd.nameVI || '',
muaTm: usd.muaTm || '',
muaCk: usd.muaCk || '',
currency: usd.currency || 'USD',
nameEN: usd.nameEN || '',
ban: usd.ban || ''
};
};
// Fetch TCB exchange rates
const fetchTCBRates = async (dateObj) => {
const dateStr = format(dateObj, 'yyyy-MM-dd');
const url = `https://techcombank.com/content/techcombank/web/vn/vi/cong-cu-tien-ich/ty-gia/_jcr_content.exchange-rates.${dateStr}.integration.json`;
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch TCB data');
const data = await res.json();
if (!data.exchangeRate?.data) return null;
// Find USD data
const usd = data.exchangeRate.data.find(item => item.label === 'USD (50,100)');
if (!usd) return null;
return {
date: formatDisplayDate(dateStr),
label: usd.label || '',
askRate: usd.askRate || '',
bidRateCK: usd.bidRateCK || '',
bidRateTM: usd.bidRateTM || '',
askRateTM: usd.askRateTM || '',
sourceCurrency: usd.sourceCurrency || 'USD',
targetCurrency: usd.targetCurrency || 'VND'
};
};
// Handle fetch button click
const handleFetch = async () => {
if (!startDate || !endDate) {
error = 'Please select both start and end dates';
return;
}
const start = new Date(startDate);
const end = new Date(endDate);
if (start > end) {
error = 'Start date must be before end date';
return;
}
const dateRange = getDateRange(start, end);
if (dateRange.length > 31) {
error = 'Date range cannot exceed 31 days';
return;
}
isLoading = true;
error = '';
results = [];
showTable = false;
try {
const fetchPromises = dateRange.map(async (date) => {
try {
if (selectedBank === 'bidv') {
return await fetchBIDVRates(date);
} else {
return await fetchTCBRates(date);
}
} catch (e) {
console.error(`Error fetching data for ${date}:`, e);
return null;
}
});
const fetchedResults = await Promise.all(fetchPromises);
results = fetchedResults.filter(Boolean);
showTable = results.length > 0;
if (results.length === 0) {
error = 'No data found for the selected date range';
}
} catch (e) {
console.error('Error fetching data:', e);
error = 'An error occurred while fetching data. Please try again.';
} finally {
isLoading = false;
}
};
// Handle export to CSV
const handleExport = () => {
if (results.length === 0) return;
let csvContent = '';
let headers = [];
if (selectedBank === 'bidv') {
headers = ['Date', 'Currency', 'Buy Cash', 'Buy Transfer', 'Sell', 'Name VI', 'Name EN'];
csvContent = headers.join(',') + '\n';
csvContent += results.map(row =>
`"${row.date}","${row.currency}","${row.muaTm}","${row.muaCk}","${row.ban}","${row.nameVI}","${row.nameEN}"`
).join('\n');
} else {
headers = ['Date', 'Label', 'Ask Rate', 'Bid Rate CK', 'Bid Rate TM', 'Ask Rate TM', 'From', 'To'];
csvContent = headers.join(',') + '\n';
csvContent += results.map(row =>
`"${row.date}","${row.label}","${row.askRate}","${row.bidRateCK}","${row.bidRateTM}","${row.askRateTM}","${row.sourceCurrency}","${row.targetCurrency}"`
).join('\n');
}
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `exchange_rates_${selectedBank}_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
</script>
<div class="container">
<h1>Exchange Rate Export</h1>
<div class="controls">
<div class="form-group">
<label for="bank">Bank:</label>
<select id="bank" bind:value={selectedBank}>
<option value="bidv">BIDV</option>
<option value="tcb">Techcombank</option>
</select>
</div>
<div class="form-group">
<label for="start-date">Start Date:</label>
<input
type="date"
id="start-date"
bind:value={startDate}
max={endDate}
>
</div>
<div class="form-group">
<label for="end-date">End Date:</label>
<input
type="date"
id="end-date"
bind:value={endDate}
min={startDate}
max={new Date().toISOString().split('T')[0]}
>
</div>
<div class="button-group">
<button class="fetch-btn" on:click={handleFetch} disabled={isLoading}>
{isLoading ? 'Fetching...' : 'Fetch Rates'}
</button>
<button
class="export-btn"
on:click={handleExport}
disabled={results.length === 0 || isLoading}
>
Export CSV
</button>
</div>
</div>
{#if error}
<div class="error">{error}</div>
{/if}
{#if isLoading}
<div class="loading">Loading data, please wait...</div>
{/if}
{#if showTable}
<div class="table-container">
{#if selectedBank === 'bidv'}
<table>
<thead>
<tr>
<th>Date</th>
<th>Currency</th>
<th>Buy Cash</th>
<th>Buy Transfer</th>
<th>Sell</th>
<th>Name (VI)</th>
<th>Name (EN)</th>
</tr>
</thead>
<tbody>
{#each results as result}
<tr>
<td>{result.date}</td>
<td>{result.currency}</td>
<td>{result.muaTm}</td>
<td>{result.muaCk}</td>
<td>{result.ban}</td>
<td>{result.nameVI}</td>
<td>{result.nameEN}</td>
</tr>
{/each}
</tbody>
</table>
{:else}
<table>
<thead>
<tr>
<th>Date</th>
<th>Label</th>
<th>Ask Rate</th>
<th>Bid Rate CK</th>
<th>Bid Rate TM</th>
<th>Ask Rate TM</th>
<th>From</th>
<th>To</th>
</tr>
</thead>
<tbody>
{#each results as result}
<tr>
<td>{result.date}</td>
<td>{result.label}</td>
<td>{result.askRate}</td>
<td>{result.bidRateCK}</td>
<td>{result.bidRateTM}</td>
<td>{result.askRateTM}</td>
<td>{result.sourceCurrency}</td>
<td>{result.targetCurrency}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
{/if}
</div>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
font-family: Arial, sans-serif;
}
h1 {
text-align: center;
margin-bottom: 2rem;
color: #2c3e50;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
align-items: flex-end;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-weight: 600;
color: #2c3e50;
}
input[type="date"],
select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.button-group {
display: flex;
gap: 1rem;
margin-left: auto;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.fetch-btn {
background-color: #3498db;
color: white;
}
.fetch-btn:hover:not(:disabled) {
background-color: #2980b9;
}
.export-btn {
background-color: #2ecc71;
color: white;
}
.export-btn:hover:not(:disabled) {
background-color: #27ae60;
}
button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.error {
color: #e74c3c;
background-color: #fadbd8;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
text-align: center;
}
.loading {
text-align: center;
color: #3498db;
margin: 2rem 0;
font-size: 1.2rem;
}
.table-container {
overflow-x: auto;
margin-top: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
table {
width: 100%;
border-collapse: collapse;
background: white;
}
th, td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #ecf0f1;
}
th {
background-color: #3498db;
color: white;
font-weight: 600;
}
tr:nth-child(even) {
background-color: #f8f9fa;
}
tr:hover {
background-color: #f1f8ff;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.button-group {
margin-left: 0;
margin-top: 1rem;
}
}
</style>
+25 -2
View File
@@ -1,2 +1,25 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<script>
import { onMount } from 'svelte';
import { format, subDays } from 'date-fns';
import ExchangeRateViewer from '$lib/ExchangeRateViewer.svelte';
let defaultStartDate = '';
let defaultEndDate = '';
onMount(() => {
const today = new Date();
const weekAgo = subDays(today, 7);
defaultStartDate = format(weekAgo, 'yyyy-MM-dd');
defaultEndDate = format(today, 'yyyy-MM-dd');
});
</script>
<main>
{#if defaultStartDate && defaultEndDate}
<ExchangeRateViewer
defaultStartDate={defaultStartDate}
defaultEndDate={defaultEndDate}
/>
{/if}
</main>
-242
View File
@@ -1,242 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exchange Rate Export - Techcombank</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
background: #f7f7f7;
}
.container {
background: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
max-width: 700px;
margin: auto;
}
h1 {
text-align: center;
}
label {
margin-right: 8px;
}
input[type="date"] {
margin-right: 16px;
padding: 4px 8px;
}
button {
padding: 8px 16px;
margin-right: 8px;
border: none;
border-radius: 4px;
background: #007bff;
color: #fff;
cursor: pointer;
font-size: 1rem;
}
button:disabled {
background: #aaa;
cursor: not-allowed;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 24px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}
th {
background: #007bff;
color: #fff;
}
.loading {
text-align: center;
margin-top: 20px;
color: #007bff;
}
.error {
color: red;
text-align: center;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Exchange Rate Export (USD, Techcombank)</h1>
<div style="margin-bottom: 20px;">
<label for="start-date">Start Date:</label>
<input type="date" id="start-date">
<label for="end-date">End Date:</label>
<input type="date" id="end-date">
<button id="fetch-btn">Fetch</button>
<button id="export-btn" disabled>Export CSV</button>
</div>
<div id="status"></div>
<table id="result-table" style="display:none;">
<thead>
<tr>
<th>Date</th>
<th>label</th>
<th>askRate</th>
<th>bidRateCK</th>
<th>bidRateTM</th>
<th>askRateTM</th>
<th>sourceCurrency</th>
<th>targetCurrency</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<script>
const fetchBtn = document.getElementById('fetch-btn');
const exportBtn = document.getElementById('export-btn');
const startDateInput = document.getElementById('start-date');
const endDateInput = document.getElementById('end-date');
const statusDiv = document.getElementById('status');
const table = document.getElementById('result-table');
const tbody = table.querySelector('tbody');
let results = [];
function formatDate(date) {
// Format as dd/MM/yyyy
const d = new Date(date);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}/${month}/${year}`;
}
function getDateRange(start, end) {
const dates = [];
let current = new Date(start);
const last = new Date(end);
while (current <= last) {
dates.push(new Date(current));
current.setDate(current.getDate() + 1);
}
return dates;
}
async function fetchUSD(dateObj) {
// Techcombank API expects yyyy-mm-dd in URL
const yyyy = dateObj.getFullYear();
const mm = String(dateObj.getMonth() + 1).padStart(2, '0');
const dd = String(dateObj.getDate()).padStart(2, '0');
const dateStr = `${yyyy}-${mm}-${dd}`;
const url = `https://techcombank.com/content/techcombank/web/vn/vi/cong-cu-tien-ich/ty-gia/_jcr_content.exchange-rates.${dateStr}.integration.json`;
try {
const res = await fetch(url);
if (!res.ok) throw new Error('API error');
const data = await res.json();
if (!data.exchangeRate || !data.exchangeRate.data) return null;
// Find USD (50,100)
const usd = data.exchangeRate.data.find(item => item.label === 'USD (50,100)');
if (!usd) return null;
return {
date: formatDate(dateObj),
label: usd.label,
askRate: usd.askRate,
bidRateCK: usd.bidRateCK,
bidRateTM: usd.bidRateTM,
askRateTM: usd.askRateTM,
sourceCurrency: usd.sourceCurrency,
targetCurrency: usd.targetCurrency
};
} catch (e) {
return null;
}
}
function renderTable(data) {
tbody.innerHTML = '';
for (const row of data) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${row.date}</td>
<td>${row.label}</td>
<td>${row.askRate || ''}</td>
<td>${row.bidRateCK || ''}</td>
<td>${row.bidRateTM || ''}</td>
<td>${row.askRateTM || ''}</td>
<td>${row.sourceCurrency}</td>
<td>${row.targetCurrency}</td>
`;
tbody.appendChild(tr);
}
table.style.display = data.length ? '' : 'none';
}
function exportCSV(data) {
const header = ['Date', 'label', 'askRate', 'bidRateCK', 'bidRateTM', 'askRateTM', 'sourceCurrency', 'targetCurrency'];
const rows = [header];
for (const row of data) {
rows.push([
row.date,
row.label,
row.askRate,
row.bidRateCK,
row.bidRateTM,
row.askRateTM,
row.sourceCurrency,
row.targetCurrency
]);
}
const csvContent = rows.map(r => r.map(cell => '"' + String(cell || '').replace(/"/g, '""') + '"').join(',')).join('\r\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'usd_exchange_rates_tcb.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
fetchBtn.onclick = async () => {
const start = startDateInput.value;
const end = endDateInput.value;
if (!start || !end) {
statusDiv.innerHTML = '<div class="error">Please select both start and end dates.</div>';
return;
}
if (start > end) {
statusDiv.innerHTML = '<div class="error">Start date must be before or equal to end date.</div>';
return;
}
statusDiv.innerHTML = '<div class="loading">Fetching data, please wait...</div>';
fetchBtn.disabled = true;
exportBtn.disabled = true;
results = [];
renderTable([]);
const dates = getDateRange(start, end);
for (let i = 0; i < dates.length; i++) {
const dateObj = dates[i];
const dateStr = formatDate(dateObj);
statusDiv.innerHTML = `<div class=\"loading\">Fetching: ${dateStr} (${i+1}/${dates.length})</div>`;
const usd = await fetchUSD(dateObj);
if (usd) {
results.push(usd);
renderTable(results);
}
}
statusDiv.innerHTML = results.length ? '' : '<div class="error">No data found for the selected range.</div>';
fetchBtn.disabled = false;
exportBtn.disabled = results.length === 0;
};
exportBtn.onclick = () => {
if (results.length) exportCSV(results);
};
</script>
</body>
</html>
+5 -1
View File
@@ -1,6 +1,10 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import devtoolsJson from 'vite-plugin-devtools-json';
export default defineConfig({
plugins: [sveltekit()]
plugins: [
devtoolsJson(),
sveltekit()
]
});