Skip to content

Conversation

@Boxuan-Matty-Lin
Copy link
Owner

This pull request introduces a major refactor and feature upgrade to the currency converter app. The main focus is on adding a responsive, interactive historical exchange rate chart for each currency, improving the sidebar's architecture, and enhancing accessibility and user experience across devices. The sidebar is now a presentational component, with all data fetching and state management moved to the page level, and several new reusable chart components are introduced.

Key changes include:

1. Historical Chart Feature & Components

2. Page Architecture & State Management

  • Refactored app/page.tsx to manage all data fetching (latest rates, history), error/loading states, and UI state (selected currency, modal visibility, desktop/mobile detection) at the page level. The sidebar now receives all data and callbacks as props, making it a pure presentational component.
  • Implemented a modal dialog for the chart on mobile, and inlined the chart on desktop, with appropriate accessibility and event handling.

3. Sidebar & Currency Card Improvements

  • Refactored CurrencySidebar to accept props for all data and event handlers, removing its own state and side effects. Exported types and constants for better type safety and reusability. [1] [2]
  • Enhanced CurrencyCard to support selection, keyboard accessibility, and click/keyboard event handlers for improved usability and a11y. [1] [2] [3]

4. Type Safety & Code Cleanliness

  • Promoted several type definitions (Rates, TargetCode, CURRENCY_META) to be exported for shared usage across components.
  • Improved prop and state typing throughout new and refactored components for better maintainability and clarity. [1] [2] [3]

References:

Copilot AI review requested due to automatic review settings October 30, 2025 01:40
@Boxuan-Matty-Lin Boxuan-Matty-Lin added documentation Improvements or additions to documentation enhancement New feature or request labels Oct 30, 2025
@github-actions
Copy link

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 60.46% 104 / 172
🔵 Statements 62.23% 117 / 188
🔵 Functions 61.11% 33 / 54
🔵 Branches 34.78% 64 / 184
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
components/sidebar/CurrencyCard.tsx 58.33% 70.37% 75% 63.63%
components/sidebar/CurrencySidebar.tsx 100% 81.81% 100% 100%
lib/server/oxr.ts 70% 41.66% 100% 77.77%
lib/server/oxr_convert.ts 100% 100% 100% 100%
lib/server/oxr_history.ts 100% 100% 100% 100%
Generated in workflow #19 for commit 829b114 by the Vitest Coverage Report Action

@Boxuan-Matty-Lin Boxuan-Matty-Lin merged commit fd3082c into main Oct 30, 2025
6 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds historical exchange rate tracking and visualization to the currency converter application. The main enhancement enables users to view time-series charts of exchange rates over the past 13 days, with interactive currency selection and responsive design that adapts between desktop and mobile views.

Key changes:

  • Adds historical rate fetching from Open Exchange Rates API with retry logic and concurrency control
  • Implements interactive chart components using Recharts with collapsible info sections
  • Refactors CurrencySidebar to be a controlled component receiving data and event handlers from parent
  • Adds currency selection feature with visual highlighting and chart synchronization

Reviewed Changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds @radix-ui/react-collapsible dependency and its transitive dependencies
package.json Registers @radix-ui/react-collapsible as a project dependency
lib/time_utils.ts Provides utilities for generating UTC date ranges for historical data fetching
lib/server/oxr_history.ts Implements historical rate fetching with retry logic, concurrent workers, and data transformation
lib/server/oxr_convert.ts Updates DEFAULTS_CURRENCY from const array to mutable array for flexible usage
lib/server/oxr.ts Adds getHistorical function and HistoricalResponse type for historical API calls
lib/server/tests/oxr_history.test.ts Comprehensive unit tests for history fetching and transformation logic
lib/server/tests/oxr.integration.test.ts Adds integration test for getHistorical endpoint with real API calls
components/ui/collapsible.tsx Wraps Radix UI collapsible primitives for consistent UI patterns
components/sidebar/tests/CurrencySidebar.test.tsx Updates tests to reflect controlled component refactor with new props
components/sidebar/tests/CurrencyCard.test.tsx Adds tests for selection interactions and keyboard navigation
components/sidebar/CurrencySidebar.tsx Refactors to controlled component accepting rates, loading state, and event handlers
components/sidebar/CurrencyCard.tsx Adds selection state, keyboard navigation, and interactive styling
components/charts/InfoCollapsible.tsx Creates reusable collapsible component for displaying data provenance information
components/charts/HistoricalChart.tsx Implements area chart with dynamic domain calculation and theme-aware tooltips
components/charts/ChartWithInfo.tsx Combines chart and collapsible info section into a single component
app/page.tsx Refactors to client component managing rates, history, selection state, and modal chart display
app/api/rates/history/route.ts Exposes historical rates API endpoint with configurable date ranges
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.




/** * Shape of Open Exchange Rates historical payload.
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment has an extra space between /** and *. Should be /** without the extra asterisk.

Suggested change
/** * Shape of Open Exchange Rates historical payload.
/**
* Shape of Open Exchange Rates historical payload.

Copilot uses AI. Check for mistakes.



/** * Fetches historical exchange rates for a specific date from Open Exchange Rates.
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment has an extra space between /** and *. Should be /** without the extra asterisk.

Suggested change
/** * Fetches historical exchange rates for a specific date from Open Exchange Rates.
/**
* Fetches historical exchange rates for a specific date from Open Exchange Rates.

Copilot uses AI. Check for mistakes.
for (const t of targets) series[t] = [];

for (const row of points) {
const d = row.date; // 已是 string
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment contains Chinese text '已是 string' (meaning 'already is string'). Should be in English: 'already a string' or removed if redundant.

Suggested change
const d = row.date; // 已是 string
const d = row.date;

Copilot uses AI. Check for mistakes.
* @returns A promise resolving to the `HistoricalResponse` payload.
*/
export async function getHistorical(dateISO: string): Promise<HistoricalResponse> {
// dateISO = YYYY-MM-DD(UTC)
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment uses Chinese full-width parentheses '(UTC)'. Should use standard ASCII parentheses: '(UTC)'.

Suggested change
// dateISO = YYYY-MM-DDUTC
// dateISO = YYYY-MM-DD (UTC)

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +92
let index = 0;

// Small worker pool that processes pending dates until exhausted.
const worker = async () => {
while (true) {
const current = index++;
if (current >= dates.length) break;
const date = dates[current];
const result = await fetchHistoricalWithRetry(date);
if (result && "rates" in result) {
points[current] = toAudRow(result.rates, upperTargets, date);
} else {
points[current] = { date, error: "fetch_failed" };
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shared index variable is mutated by multiple concurrent workers without synchronization. While JavaScript's single-threaded event loop prevents true race conditions, the post-increment index++ is not atomic in async contexts. This could lead to duplicate work or skipped indices if workers interleave. Consider using a proper queue or generator pattern for work distribution.

Suggested change
let index = 0;
// Small worker pool that processes pending dates until exhausted.
const worker = async () => {
while (true) {
const current = index++;
if (current >= dates.length) break;
const date = dates[current];
const result = await fetchHistoricalWithRetry(date);
if (result && "rates" in result) {
points[current] = toAudRow(result.rates, upperTargets, date);
} else {
points[current] = { date, error: "fetch_failed" };
// Use a queue to safely distribute work among workers.
const dateQueue = dates.map((date, idx) => ({ date, idx }));
// Small worker pool that processes pending dates until exhausted.
const worker = async () => {
while (true) {
const item = dateQueue.shift();
if (!item) break;
const { date, idx } = item;
const result = await fetchHistoricalWithRetry(date);
if (result && "rates" in result) {
points[idx] = toAudRow(result.rates, upperTargets, date);
} else {
points[idx] = { date, error: "fetch_failed" };

Copilot uses AI. Check for mistakes.
decimals = 4,
className,
}: Props) {
// 最后一个点,用于高亮当前值
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment contains Chinese text '最后一个点,用于高亮当前值' (meaning 'last point, used to highlight current value'). Should be in English: 'Last point, used to highlight the current value'.

Suggested change
// 最后一个点,用于高亮当前值
// Last point, used to highlight the current value

Copilot uses AI. Check for mistakes.
<div className="h-72 w-full">
<ResponsiveContainer>
<AreaChart data={data}>
{/* 时间轴保持简洁,不改默认配色 */}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment contains Chinese text '时间轴保持简洁,不改默认配色' (meaning 'Keep time axis simple, don't change default colors'). Should be in English: 'Keep time axis simple, use default colors'.

Suggested change
{/* 时间轴保持简洁,不改默认配色 */}
{/* Keep time axis simple, use default colors */}

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +101
{/* 只调整 Tooltip 尺寸与圆角
暗黑模式下灰底和灰白字,其余保持原生 */}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment contains Chinese text '只调整 Tooltip 尺寸与圆角 暗黑模式下灰底和灰白字,其余保持原生' (meaning 'Only adjust Tooltip size and border-radius; in dark mode use gray background and light text, keep rest native'). Should be in English: 'Only adjust Tooltip size and border-radius; in dark mode use gray background and light text, keep rest native'.

Suggested change
{/* Tooltip
暗黑模式下灰底和灰白字,其余保持原生 */}
{/* Only adjust Tooltip size and border-radius
in dark mode use gray background and light text, keep rest native */}

Copilot uses AI. Check for mistakes.
fill="url(#fxFill)"
/>

{/* 渐变不改颜色,仅保留透明度过渡 */}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment contains Chinese text '渐变不改颜色,仅保留透明度过渡' (meaning 'Gradient keeps color, only varies opacity'). Should be in English: 'Gradient keeps color, only varies opacity'.

Suggested change
{/* 渐变不改颜色,仅保留透明度过渡 */}
{/* Gradient keeps color, only varies opacity */}

Copilot uses AI. Check for mistakes.
</ResponsiveContainer>
</div>

{/* 末端小圆点的文本提示 */}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment contains Chinese text '末端小圆点的文本提示' (meaning 'Text label for the last point'). Should be in English: 'Text label for the last point'.

Suggested change
{/* 末端小圆点的文本提示 */}
{/* Text label for the last point */}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants