feat(nextjs): migrate from raw html to nextjs

This commit is contained in:
tiennm99 committed 2025-06-26 20:24:06 +07:00
1 parent 8d7eff85e3
commit 68eb284362
6 files changed
+389 -870

No files matched your search

+187
View File
@@ -0,0 +1,187 @@
"use client";
import { useState } from "react";
export default function BidvPage() {
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [status, setStatus] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
function formatDate(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}`;
}
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;
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;
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) {
return (
<table style={{ width: "100%", borderCollapse: "collapse", marginTop: 24, display: data.length ? "" : "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>
{data.map((row, i) => (
<tr key={i}>
<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>
</tr>
))}
</tbody>
</table>
);
}
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);
}
async function handleFetch() {
if (!startDate || !endDate) {
setStatus("<div class='error'>Please select both start and end dates.</div>");
return;
}
if (startDate > endDate) {
setStatus("<div class='error'>Start date must be before or equal to end date.</div>");
return;
}
setStatus("<div class='loading'>Fetching data, please wait...</div>");
setLoading(true);
setResults([]);
const dates = getDateRange(startDate, endDate);
const newResults = [];
for (let i = 0; i < dates.length; i++) {
const dateObj = dates[i];
const dateStr = formatDate(dateObj);
setStatus(`<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) {
newResults.push({ date: dateStr, ...usd });
setResults([...newResults]);
}
}
setStatus(newResults.length ? "" : "<div class='error'>No data found for the selected range.</div>");
setLoading(false);
}
return (
<div style={{ background: "#fff", padding: 24, borderRadius: 8, boxShadow: "0 2px 8px rgba(0,0,0,0.07)", maxWidth: 700, margin: "auto" }}>
<h1>Exchange Rate Export (USD)</h1>
<div style={{ marginBottom: 20 }}>
<label htmlFor="start-date">Start Date:</label>
<input type="date" id="start-date" value={startDate} onChange={e => setStartDate(e.target.value)} />
<label htmlFor="end-date">End Date:</label>
<input type="date" id="end-date" value={endDate} onChange={e => setEndDate(e.target.value)} />
<button onClick={handleFetch} disabled={loading}>Fetch</button>
<button onClick={() => exportCSV(results)} disabled={!results.length}>Export CSV</button>
</div>
<div dangerouslySetInnerHTML={{ __html: status }} />
{renderTable(results)}
</div>
);
}
+34 -89
View File
@@ -1,95 +1,40 @@
import Image from "next/image";
import styles from "./page.module.css";
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function Home() {
return (
<div className={styles.page}>
<main className={styles.main}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol>
<li>
Get started by editing <code>src/app/page.js</code>.
</li>
<li>Save and see your changes instantly.</li>
</ol>
const router = useRouter();
const [bank, setBank] = useState("bidv");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
<div className={styles.ctas}>
<a
className={styles.primary}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className={styles.logo}
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
className={styles.secondary}
>
Read our docs
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
function handleBankChange(e) {
setBank(e.target.value);
if (e.target.value === "bidv") {
router.push("/bidv");
} else if (e.target.value === "tcb") {
router.push("/tcb");
}
}
return (
<div style={{ background: "#fff", padding: 24, borderRadius: 8, boxShadow: "0 2px 8px rgba(0,0,0,0.07)", maxWidth: 700, margin: "auto" }}>
<h1>Exchange Rate Export (USD)</h1>
<div style={{ marginBottom: 20 }}>
<label htmlFor="start-date">Start Date:</label>
<input type="date" id="start-date" value={startDate} onChange={e => setStartDate(e.target.value)} />
<label htmlFor="end-date">End Date:</label>
<input type="date" id="end-date" value={endDate} onChange={e => setEndDate(e.target.value)} />
<button onClick={() => router.push(`/${bank}`)} disabled={!startDate || !endDate}>Go</button>
<label htmlFor="bank-select">Bank:</label>
<select id="bank-select" value={bank} onChange={handleBankChange}>
<option value="bidv">BIDV</option>
<option value="tcb">Techcombank</option>
</select>
</div>
<div style={{ color: '#888', textAlign: 'center', marginTop: 40 }}>
Select a bank and date range, then click Go to view and export exchange rates.
</div>
</div>
);
}
+163
View File
@@ -0,0 +1,163 @@
"use client";
import { useState } from "react";
export default function TcbPage() {
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [status, setStatus] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
function formatDate(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}`;
}
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) {
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;
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) {
return (
<table style={{ width: "100%", borderCollapse: "collapse", marginTop: 24, display: data.length ? "" : "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>
{data.map((row, i) => (
<tr key={i}>
<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>
</tr>
))}
</tbody>
</table>
);
}
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);
}
async function handleFetch() {
if (!startDate || !endDate) {
setStatus("<div class='error'>Please select both start and end dates.</div>");
return;
}
if (startDate > endDate) {
setStatus("<div class='error'>Start date must be before or equal to end date.</div>");
return;
}
setStatus("<div class='loading'>Fetching data, please wait...</div>");
setLoading(true);
setResults([]);
const dates = getDateRange(startDate, endDate);
const newResults = [];
for (let i = 0; i < dates.length; i++) {
const dateObj = dates[i];
const dateStr = formatDate(dateObj);
setStatus(`<div class='loading'>Fetching: ${dateStr} (${i + 1}/${dates.length})</div>`);
const usd = await fetchUSD(dateObj);
if (usd) {
newResults.push(usd);
setResults([...newResults]);
}
}
setStatus(newResults.length ? "" : "<div class='error'>No data found for the selected range.</div>");
setLoading(false);
}
return (
<div style={{ background: "#fff", padding: 24, borderRadius: 8, boxShadow: "0 2px 8px rgba(0,0,0,0.07)", maxWidth: 700, margin: "auto" }}>
<h1>Exchange Rate Export (USD, Techcombank)</h1>
<div style={{ marginBottom: 20 }}>
<label htmlFor="start-date">Start Date:</label>
<input type="date" id="start-date" value={startDate} onChange={e => setStartDate(e.target.value)} />
<label htmlFor="end-date">End Date:</label>
<input type="date" id="end-date" value={endDate} onChange={e => setEndDate(e.target.value)} />
<button onClick={handleFetch} disabled={loading}>Fetch</button>
<button onClick={() => exportCSV(results)} disabled={!results.length}>Export CSV</button>
</div>
<div dangerouslySetInnerHTML={{ __html: status }} />
{renderTable(results)}
</div>
);
}