Merge branch 'refactor/source-cleanup-and-viewer-split'

History-preserving absorb: the branch's content lands in this merge
commit and is restored to the previous main tree in the next commit.
The branch ref is deleted after this merge; its history stays reachable
through the second parent.
This commit is contained in:
tiennm99 committed 2026-08-07 13:44:56 +07:00
commit 32123499cb
9 files changed
+784 -1040

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
{
"permissions": {
"allow": [
"Read",
"Edit",
"Bash(git status *)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git branch *)",
"Bash(git checkout *)",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(git stash *)",
"Bash(git merge *)",
"Bash(git rebase *)",
"Bash(git fetch *)",
"Bash(git pull *)",
"Bash(git show *)",
"Bash(git blame *)",
"Bash(ls *)",
"Bash(cat *)",
"Bash(find *)",
"Bash(grep *)",
"Bash(rg *)",
"Bash(head *)",
"Bash(tail *)",
"Bash(wc *)",
"Bash(sort *)",
"Bash(uniq *)",
"Bash(diff *)",
"Bash(echo *)",
"Bash(pwd)",
"Bash(which *)",
"Bash(mkdir *)",
"Bash(cp *)",
"Bash(mv *)",
"Bash(touch *)",
"Bash(cd *)",
"Bash(npm run *)",
"Bash(npm test *)",
"Bash(npm install *)",
"Bash(npx *)",
"Bash(yarn *)",
"Bash(pnpm *)",
"Bash(gradle *)",
"Bash(./gradlew *)",
"Bash(go build *)",
"Bash(go test *)",
"Bash(go run *)",
"Bash(go mod *)",
"Bash(cargo *)",
"Bash(make *)",
"Bash(python *)",
"Bash(pip *)",
"Bash(node *)",
"Bash(java *)",
"Bash(javac *)",
"Bash(sed *)",
"Bash(awk *)",
"Bash(jq *)",
"Bash(tree *)",
"Bash(date *)",
"Bash(env *)",
"Bash(printenv *)",
"Bash(realpath *)",
"Bash(dirname *)",
"Bash(basename *)",
"Bash(stat *)",
"Bash(file *)",
"Bash(xargs *)",
"Bash(tee *)"
],
"deny": [
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(git push * --force *)",
"Bash(git push * -f *)",
"Bash(rm -rf /)",
"Bash(rm -rf /*)",
"Bash(rm -rf ~)",
"Bash(rm -rf ~/*)",
"Bash(curl * | bash *)",
"Bash(curl * | sh *)",
"Bash(wget * | bash *)",
"Bash(wget * | sh *)"
]
}
}
+18
View File
@@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
minor-and-patch:
update-types: ["minor", "patch"]
security:
applies-to: security-updates
patterns: ["*"]
versioning-strategy: increase
labels: ["dependencies", "security"]
commit-message:
prefix: "chore"
include: "scope"
+4
View File
@@ -1,3 +1,7 @@
.claude/session-state
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
+11 -11
View File
@@ -1,14 +1,14 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const eslintConfig = defineConfig([
...nextVitals,
{
rules: {
"react-hooks/set-state-in-effect": "off",
},
},
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
]);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [...compat.extends("next/core-web-vitals")];
export default eslintConfig;
+16 -12
View File
@@ -10,24 +10,28 @@
"lint": "eslint src"
},
"dependencies": {
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"axios": "^1.18.1",
"date-fns": "^4.4.0",
"next": "16.2.11",
"react": "^19.2.7",
"react-datepicker": "^9.1.0",
"react-dom": "^19.2.7",
"recharts": "^3.9.2",
"write-excel-file": "^4.1.1"
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"axios": "^1.12.0",
"date-fns": "^4.1.0",
"next": "16.2.3",
"react": "^19.0.0",
"react-datepicker": "^8.4.0",
"react-dom": "^19.0.0",
"recharts": "^3.8.1",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"eslint": "^9.39.5",
"eslint-config-next": "16.2.11",
"eslint": "^9",
"eslint-config-next": "15.3.4",
"tailwindcss": "^4"
},
"engines": {
"node": ">=24"
},
"pnpm": {
"ignoredBuiltDependencies": ["sharp", "unrs-resolver"]
}
}
+630 -979
View File
File diff suppressed because it is too large. Load diff
-13
View File
@@ -1,16 +1,3 @@
overrides:
'@babel/core': 7.29.6
form-data: 4.0.6
js-yaml: 4.3.0
postcss: 8.5.18
# next 16.2.11 declares sharp ^0.34.5, which excludes the patched 0.35.0, so
# no upstream bump can reach the fix -- force it here.
sharp: 0.35.0
# Two independent major branches reach this package: minimatch@3 pulls 1.x,
# minimatch@10 pulls 5.x. Each branch needs its own patched version.
'brace-expansion@1': 1.1.16
'brace-expansion@5': 5.0.7
allowBuilds:
sharp: true
unrs-resolver: true
@@ -1,20 +0,0 @@
export function rowsToCsv(rows) {
return `${rows.map((row) => row.map(formatCsvCell).join(",")).join("\r\n")}\r\n`;
}
export function downloadTextFile(text, filename, type) {
const blob = new Blob([text], { type });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function formatCsvCell(value) {
const text = value == null ? "" : String(value);
return /[",\r\n]/.test(text) ? `"${text.replaceAll("\"", "\"\"")}"` : text;
}
+17 -5
View File
@@ -4,14 +4,13 @@ import { useState, useEffect, useCallback } from "react";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { format, subDays, differenceInCalendarDays } from "date-fns";
import writeExcelFile from "write-excel-file/browser";
import * as XLSX from "xlsx";
import { LoadingSpinner, ErrorMessage, EmptyState } from "./exchange-rate-status";
import { RateTable, ComparisonTable } from "./exchange-rate-table";
import { RateTrendChart } from "./exchange-rate-chart";
import { SunIcon, MoonIcon, SystemIcon, FetchIcon, ExcelIcon, CsvIcon } from "./exchange-rate-icons";
import { loadFromUrl, loadSavedSettings, saveSettings, applyTheme } from "./exchange-rate-storage";
import { CURRENCY_OPTIONS, DATE_PRESETS, getActivePreset, THEME_CYCLE, THEME_LABELS } from "./exchange-rate-constants";
import { downloadTextFile, rowsToCsv } from "./exchange-rate-export-files";
export default function ExchangeRateViewer() {
const [startDate, setStartDate] = useState(() => subDays(new Date(), 7));
@@ -136,14 +135,27 @@ export default function ExchangeRateViewer() {
const baseFilename = `exchange_rates_${compareMode ? "compare" : selectedBank}_${selectedCurrency}_${format(new Date(), "yyyy-MM-dd")}`;
const handleExportExcel = async () => {
const handleExportExcel = () => {
if (results.length === 0) return;
await writeExcelFile(getExportRows(), { sheet: "Exchange Rates" }).toFile(`${baseFilename}.xlsx`);
const ws = XLSX.utils.aoa_to_sheet(getExportRows());
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Exchange Rates");
XLSX.writeFile(wb, `${baseFilename}.xlsx`);
};
const handleExportCsv = () => {
if (results.length === 0) return;
downloadTextFile(rowsToCsv(getExportRows()), `${baseFilename}.csv`, "text/csv;charset=utf-8;");
const ws = XLSX.utils.aoa_to_sheet(getExportRows());
const csv = XLSX.utils.sheet_to_csv(ws);
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${baseFilename}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const activePreset = getActivePreset(startDate, endDate);