mirror of
https://github.com/tiennm99/exchange-rate-export.git
synced 2026-08-05 10:23:38 +00:00
feat(nextjs): migrate old logic
This commit is contained in:
Generated
+113
-14
@@ -8,6 +8,8 @@
|
||||
"name": "exchange-rate-export",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "15.3.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
@@ -2110,6 +2112,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||
@@ -2136,6 +2144,17 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -2211,7 +2230,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -2346,6 +2364,18 @@
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -2429,6 +2459,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -2490,6 +2530,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
@@ -2517,7 +2566,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@@ -2622,7 +2670,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2632,7 +2679,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2670,7 +2716,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@@ -2683,7 +2728,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -3290,6 +3334,26 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@@ -3306,11 +3370,26 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -3351,7 +3430,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@@ -3376,7 +3454,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
@@ -3464,7 +3541,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3543,7 +3619,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3556,7 +3631,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -3572,7 +3646,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -4486,7 +4559,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4516,6 +4588,27 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -5019,6 +5112,12 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
+6
-4
@@ -9,15 +9,17 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "15.3.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"next": "15.3.4"
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.4",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"tailwindcss": "^4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { format } from 'date-fns';
|
||||
import axios from 'axios';
|
||||
import https from 'https';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// Configure axios with legacy SSL support
|
||||
const axiosInstance = axios.create({
|
||||
httpsAgent: new https.Agent({
|
||||
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||
})
|
||||
});
|
||||
|
||||
// Helper function to fetch BIDV exchange rates
|
||||
async function fetchBIDVRates(dateObj) {
|
||||
const dateStr = format(dateObj, 'dd/MM/yyyy');
|
||||
try {
|
||||
// First API call to get namerecord
|
||||
const timeRes = await axiosInstance.get(`https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet?date=${dateStr}`);
|
||||
const timeData = timeRes.data;
|
||||
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 axiosInstance.get(`https://bidv.com.vn/ServicesBIDV/ExchangeDetailServlet?date=${dateStr}&time=${latest.namerecord}`);
|
||||
const rateData = rateRes.data;
|
||||
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 || ''
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching BIDV rates:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to fetch TCB exchange rates
|
||||
async function fetchTCBRates(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`;
|
||||
try {
|
||||
const res = await axiosInstance.get(url);
|
||||
const data = res.data;
|
||||
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: dateStr,
|
||||
label: usd.label || '',
|
||||
askRate: usd.askRate || '',
|
||||
bidRateCK: usd.bidRateCK || '',
|
||||
bidRateTM: usd.bidRateTM || '',
|
||||
askRateTM: usd.askRateTM || '',
|
||||
sourceCurrency: usd.sourceCurrency || 'USD',
|
||||
targetCurrency: usd.targetCurrency || 'VND'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching TCB rates:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get date range between start and end dates
|
||||
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;
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { startDate, endDate, bank } = body;
|
||||
if (!startDate || !endDate || !bank) {
|
||||
return new Response(JSON.stringify({ error: 'Missing required parameters' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
const dates = getDateRange(new Date(startDate), new Date(endDate));
|
||||
const results = [];
|
||||
for (const date of dates) {
|
||||
let rate = null;
|
||||
try {
|
||||
if (bank === 'bidv') {
|
||||
rate = await fetchBIDVRates(date);
|
||||
} else if (bank === 'tcb') {
|
||||
rate = await fetchTCBRates(date);
|
||||
}
|
||||
if (rate) {
|
||||
results.push(rate);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching rates for ${date}:`, error);
|
||||
}
|
||||
}
|
||||
return new Response(JSON.stringify({ data: results }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
} catch (error) {
|
||||
console.error('Server error:', error);
|
||||
return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
function formatDisplayDate(dateStr) {
|
||||
try {
|
||||
let date;
|
||||
if (dateStr.includes('/')) {
|
||||
const [day, month, year] = dateStr.split('/');
|
||||
date = new Date(`${year}-${month}-${day}`);
|
||||
} else {
|
||||
date = new Date(dateStr);
|
||||
}
|
||||
return format(date, 'dd/MM/yyyy');
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate }) {
|
||||
const [startDate, setStartDate] = useState(defaultStartDate);
|
||||
const [endDate, setEndDate] = useState(defaultEndDate);
|
||||
const [selectedBank, setSelectedBank] = useState('bidv');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
const [showTable, setShowTable] = useState(false);
|
||||
|
||||
const handleFetch = async () => {
|
||||
if (!startDate || !endDate) {
|
||||
setError('Please select both start and end dates');
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
setResults([]);
|
||||
setShowTable(false);
|
||||
try {
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
if (start > end) {
|
||||
setError('Start date cannot be after end date');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/exchange-rates', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
startDate: start.toISOString().split('T')[0],
|
||||
endDate: end.toISOString().split('T')[0],
|
||||
bank: selectedBank
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to fetch exchange rates');
|
||||
}
|
||||
const data = await response.json();
|
||||
setResults(data.data || []);
|
||||
setShowTable(true);
|
||||
} catch (err) {
|
||||
setError(`An error occurred while fetching exchange rates: ${err.message}`);
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 max-w-2xl">
|
||||
<h1 className="text-2xl font-bold mb-4">Exchange Rate Export</h1>
|
||||
<div className="controls grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
|
||||
<div className="form-group flex flex-col">
|
||||
<label htmlFor="bank" className="mb-1">Bank:</label>
|
||||
<select id="bank" value={selectedBank} onChange={e => setSelectedBank(e.target.value)} className="border rounded p-2">
|
||||
<option value="bidv">BIDV</option>
|
||||
<option value="tcb">Techcombank</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group flex flex-col">
|
||||
<label htmlFor="start-date" className="mb-1">Start Date:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="start-date"
|
||||
value={startDate}
|
||||
max={endDate}
|
||||
onChange={e => setStartDate(e.target.value)}
|
||||
className="border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group flex flex-col">
|
||||
<label htmlFor="end-date" className="mb-1">End Date:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="end-date"
|
||||
value={endDate}
|
||||
min={startDate}
|
||||
max={new Date().toISOString().split('T')[0]}
|
||||
onChange={e => setEndDate(e.target.value)}
|
||||
className="border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="button-group flex gap-2 items-end">
|
||||
<button className="fetch-btn bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50" onClick={handleFetch} disabled={isLoading}>
|
||||
{isLoading ? 'Fetching...' : 'Fetch Rates'}
|
||||
</button>
|
||||
<button
|
||||
className="export-btn bg-green-600 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
onClick={handleExport}
|
||||
disabled={results.length === 0 || isLoading}
|
||||
>
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className="error text-red-600 mb-2">{error}</div>}
|
||||
{isLoading && <div className="loading mb-2">Loading data, please wait...</div>}
|
||||
{showTable && (
|
||||
<div className="table-container overflow-x-auto mt-4">
|
||||
{selectedBank === 'bidv' ? (
|
||||
<table className="min-w-full border border-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border px-2 py-1">Date</th>
|
||||
<th className="border px-2 py-1">Currency</th>
|
||||
<th className="border px-2 py-1">Buy Cash</th>
|
||||
<th className="border px-2 py-1">Buy Transfer</th>
|
||||
<th className="border px-2 py-1">Sell</th>
|
||||
<th className="border px-2 py-1">Name (VI)</th>
|
||||
<th className="border px-2 py-1">Name (EN)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map((result, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="border px-2 py-1">{result.date}</td>
|
||||
<td className="border px-2 py-1">{result.currency}</td>
|
||||
<td className="border px-2 py-1">{result.muaTm}</td>
|
||||
<td className="border px-2 py-1">{result.muaCk}</td>
|
||||
<td className="border px-2 py-1">{result.ban}</td>
|
||||
<td className="border px-2 py-1">{result.nameVI}</td>
|
||||
<td className="border px-2 py-1">{result.nameEN}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<table className="min-w-full border border-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border px-2 py-1">Date</th>
|
||||
<th className="border px-2 py-1">Label</th>
|
||||
<th className="border px-2 py-1">Ask Rate</th>
|
||||
<th className="border px-2 py-1">Bid Rate CK</th>
|
||||
<th className="border px-2 py-1">Bid Rate TM</th>
|
||||
<th className="border px-2 py-1">Ask Rate TM</th>
|
||||
<th className="border px-2 py-1">From</th>
|
||||
<th className="border px-2 py-1">To</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map((result, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="border px-2 py-1">{result.date}</td>
|
||||
<td className="border px-2 py-1">{result.label}</td>
|
||||
<td className="border px-2 py-1">{result.askRate}</td>
|
||||
<td className="border px-2 py-1">{result.bidRateCK}</td>
|
||||
<td className="border px-2 py-1">{result.bidRateTM}</td>
|
||||
<td className="border px-2 py-1">{result.askRateTM}</td>
|
||||
<td className="border px-2 py-1">{result.sourceCurrency}</td>
|
||||
<td className="border px-2 py-1">{result.targetCurrency}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+22
-95
@@ -1,103 +1,30 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ExchangeRateViewer from "./components/ExchangeRateViewer";
|
||||
import { format, subDays } from "date-fns";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
||||
src/app/page.js
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
const [defaultStartDate, setDefaultStartDate] = useState("");
|
||||
const [defaultEndDate, setDefaultEndDate] = useState("");
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
const today = new Date();
|
||||
const weekAgo = subDays(today, 7);
|
||||
setDefaultStartDate(format(weekAgo, "yyyy-MM-dd"));
|
||||
setDefaultEndDate(format(today, "yyyy-MM-dd"));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-8 sm:p-20 bg-background text-foreground font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="w-full flex flex-col items-center">
|
||||
{defaultStartDate && defaultEndDate && (
|
||||
<ExchangeRateViewer
|
||||
defaultStartDate={defaultStartDate}
|
||||
defaultEndDate={defaultEndDate}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&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
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&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
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user