Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enabled viewing historical finance data #3658

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
96 changes: 74 additions & 22 deletions components/FinancialSummary/BarChartComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use client';

import React, { useEffect, useState } from 'react';
import { Bar, BarChart, CartesianGrid, Legend, Tooltip, YAxis } from 'recharts';

import type { ExpenseItem, ExpensesLinkItem } from '@/types/FinancialSummary/BarChartComponent';
import { loadYearData } from '@/utils/loadYearData';

import ExpensesData from '../../config/finance/json-data/Expenses.json';
import ExpensesLinkData from '../../config/finance/json-data/ExpensesLink.json';
import ExpensesData from '../../config/finance/json-data/2024/Expenses.json';
import ExpensesLinkData from '../../config/finance/json-data/2024/ExpensesLink.json';
import { getUniqueCategories } from '../../utils/getUniqueCategories';
import CustomTooltip from './CustomTooltip';
import ExpensesCard from './ExpensesCard';
Expand All @@ -13,35 +16,67 @@ import ExpensesCard from './ExpensesCard';
* @description BarChartComponent component displays a bar chart for expense analysis.
*/
export default function BarChartComponent() {
const [mounted, setMounted] = useState(false);
// Setting up state variables using useState hook
const [selectedCategory, setSelectedCategory] = useState<string>('All Categories');
const [selectedMonth, setSelectedMonth] = useState<string>('All Months');
const [selectedYear, setSelectedYear] = useState<string>('2024');
const [windowWidth, setWindowWidth] = useState<number>(0);
const [currentData, setCurrentData] = useState<{
expensesData: { [key: string]: ExpenseItem[] };
expensesLinkData: ExpensesLinkItem[];
}>({
expensesData: ExpensesData, // Use JSON data as initial value
expensesLinkData: ExpensesLinkData
});

// Extracting unique categories and months from the data
const categories: string[] = getUniqueCategories();
const months: string[] = Object.keys(ExpensesData);
// Extracting unique categories from the data
const categories: string[] = getUniqueCategories({ selectedYear, selectedMonth });
const years: string[] = ['2023', '2024']; // Add more years as needed

// Effect hook to update windowWidth state on resize
// eslint-disable-next-line consistent-return
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};

// Initial setup and event listener
handleResize();
window.addEventListener('resize', handleResize);

// Cleanup function to remove event listener
return () => {
window.removeEventListener('resize', handleResize);
};
setMounted(true);
if (typeof window !== 'undefined') {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};

handleResize();
window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}
}, []);

// Filtering data based on selected month and category
const filteredData: ExpenseItem[] = Object.entries(ExpensesData).flatMap(([month, entries]) =>
// Effect to load year-specific data when year changes
useEffect(() => {
// Load data for the selected year (or All_years)
const { expensesData, expensesLinkData } = loadYearData(selectedYear === 'All Years' ? 'All Years' : selectedYear);

if (Object.keys(expensesData).length === 0) {
// If no data found, fallback to default data
setCurrentData({
expensesData: ExpensesData,
expensesLinkData: ExpensesLinkData
});
} else {
setCurrentData({ expensesData, expensesLinkData });
}
}, [selectedYear]);
Comment on lines +56 to +69
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling to year data loading effect.

The year data loading effect should handle potential errors and provide user feedback.

 useEffect(() => {
+  const [error, setError] = useState<Error | null>(null);
+  setError(null);
+  try {
     const { expensesData, expensesLinkData } = loadYearData(selectedYear === 'All Years' ? 'All Years' : selectedYear);
 
     if (Object.keys(expensesData).length === 0) {
       setCurrentData({
         expensesData: ExpensesData,
         expensesLinkData: ExpensesLinkData
       });
     } else {
       setCurrentData({ expensesData, expensesLinkData });
     }
+  } catch (err) {
+    setError(err instanceof Error ? err : new Error('Failed to load data'));
+    // Fallback to default data
+    setCurrentData({
+      expensesData: ExpensesData,
+      expensesLinkData: ExpensesLinkData
+    });
+  }
 }, [selectedYear]);
+
+if (error) {
+  // Show error toast or notification
+  console.error('Error loading year data:', error);
+}

Committable suggestion skipped: line range outside the PR's diff.


// Modify months to use current year's data
const months: string[] = Object.keys(currentData.expensesData);

// Filtering data based on selected month, year, and category
const filteredData: ExpenseItem[] = Object.entries(currentData.expensesData).flatMap(([month, entries]) =>
selectedMonth === 'All Months' || selectedMonth === month
? entries.filter((entry) => selectedCategory === 'All Categories' || entry.Category === selectedCategory)
? entries.filter(
(entry: ExpenseItem) => selectedCategory === 'All Categories' || entry.Category === selectedCategory
)
: []
);

Expand All @@ -68,6 +103,11 @@ export default function BarChartComponent() {
const barWidth: number | undefined = windowWidth && windowWidth < 900 ? undefined : 800;
const barHeight: number | undefined = windowWidth && windowWidth < 900 ? undefined : 400;

// Don't render anything until component is mounted
if (!mounted) {
return null;
}

return (
<div className='mt-8 flex items-center justify-center sm:px-6 lg:px-8'>
<div className='w-full px-4 text-center lg:w-2/3'>
Expand Down Expand Up @@ -107,6 +147,18 @@ export default function BarChartComponent() {
</option>
))}
</select>
<select
className='m-1 w-full rounded-md border border-gray-600 bg-white p-2 text-xs font-semibold text-violet sm:w-auto md:w-48'
value={selectedYear}
onChange={(e) => setSelectedYear(e.target.value)}
>
<option value='All Years'>All Years</option>
{years.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
</div>
</div>
</div>
Expand All @@ -122,7 +174,7 @@ export default function BarChartComponent() {
fill='#7B5DD3FF'
onClick={(data) => {
const category = data.payload.Category;
const matchedLinkObject: ExpensesLinkItem | undefined = ExpensesLinkData.find(
const matchedLinkObject: ExpensesLinkItem | undefined = currentData.expensesLinkData.find(
(obj) => obj.category === category
);

Expand All @@ -133,7 +185,7 @@ export default function BarChartComponent() {
/>
</BarChart>
</div>
{windowWidth && windowWidth < 900 ? <ExpensesCard /> : null}
{windowWidth && windowWidth < 900 ? <ExpensesCard year={selectedYear} /> : null}
</div>
</div>
);
Expand Down
22 changes: 14 additions & 8 deletions components/FinancialSummary/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import React from 'react';

import type { ExpenseItem, Expenses } from '@/types/FinancialSummary/BarChartComponent';

import ExpensesLinkData from '../../config/finance/json-data/ExpensesLink.json';
import type { ExpenseItem, Expenses, ExpensesLink } from '@/types/FinancialSummary/BarChartComponent';

/**
* @description Card component displays expense details for a specific month.
* @param {Object} props - Props for Card component.
* @param {string} props.month - Month for which expenses are displayed.
* @param {ExpenseItem[]} props.data - Expense data for the month.
* @param {string} month - Month for which expenses are displayed.
* @param {ExpenseItem[]} data - Expense data for the month.
* @param {ExpensesLink} expensesLinkData - Expense link data for the month.
*/
export default function Card({ month, data }: { month: keyof Expenses; data: ExpenseItem[] }) {
export default function Card({
month,
data,
expensesLinkData
}: {
month: keyof Expenses;
data: ExpenseItem[];
expensesLinkData: ExpensesLink;
}) {
/**
* Handles the click event on an expense category.
* Opens a new window with the corresponding link if available.
* @param {string} category - The expense category clicked.
* {void}
*/
function handleExpenseClick(category: string) {
const matchedLinkObject = ExpensesLinkData.find((obj) => obj.category === category);
const matchedLinkObject = expensesLinkData.find((obj) => obj.category === category);

if (matchedLinkObject) {
window.open(matchedLinkObject.link, '_blank');
Expand Down
11 changes: 7 additions & 4 deletions components/FinancialSummary/ExpensesCard.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import React from 'react';

import type { Expenses } from '@/types/FinancialSummary/BarChartComponent';
import { loadYearData } from '@/utils/loadYearData';

import ExpensesData from '../../config/finance/json-data/Expenses.json';
import Card from './Card';

/**
* @description ExpensesCard component displays all expenses for each month.
* @param {string} year - The year for which expenses are to be displayed.
*/
export default function ExpensesCard() {
export default function ExpensesCard({ year }: { year: string }) {
const { expensesData, expensesLinkData } = loadYearData(year);

return (
<div className='overflow-x-auto'>
<div className='grid auto-cols-max grid-flow-col gap-4 p-4'>
{Object.entries(ExpensesData).map(function ([month, data], index) {
return <Card key={index} month={month as keyof Expenses} data={data} />;
{Object.entries(expensesData).map(function ([month, data], index) {
return <Card key={index} month={month as keyof Expenses} data={data} expensesLinkData={expensesLinkData} />;
Comment on lines +13 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for data loading.

Consider handling potential errors when loading year data and provide a fallback UI.

-  const { expensesData, expensesLinkData } = loadYearData(year);
+  const [error, setError] = useState<Error | null>(null);
+  const [data, setData] = useState<ReturnType<typeof loadYearData> | null>(null);
+
+  useEffect(() => {
+    try {
+      const yearData = loadYearData(year);
+      setData(yearData);
+      setError(null);
+    } catch (err) {
+      setError(err instanceof Error ? err : new Error('Failed to load data'));
+      setData(null);
+    }
+  }, [year]);
+
+  if (error) {
+    return <div className="text-red-500">Error loading data: {error.message}</div>;
+  }
+
+  if (!data) {
+    return <div>Loading...</div>;
+  }
+
+  const { expensesData, expensesLinkData } = data;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { expensesData, expensesLinkData } = loadYearData(year);
return (
<div className='overflow-x-auto'>
<div className='grid auto-cols-max grid-flow-col gap-4 p-4'>
{Object.entries(ExpensesData).map(function ([month, data], index) {
return <Card key={index} month={month as keyof Expenses} data={data} />;
{Object.entries(expensesData).map(function ([month, data], index) {
return <Card key={index} month={month as keyof Expenses} data={data} expensesLinkData={expensesLinkData} />;
const [error, setError] = useState<Error | null>(null);
const [data, setData] = useState<ReturnType<typeof loadYearData> | null>(null);
useEffect(() => {
try {
const yearData = loadYearData(year);
setData(yearData);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to load data'));
setData(null);
}
}, [year]);
if (error) {
return <div className="text-red-500">Error loading data: {error.message}</div>;
}
if (!data) {
return <div>Loading...</div>;
}
const { expensesData, expensesLinkData } = data;
return (
<div className='overflow-x-auto'>
<div className='grid auto-cols-max grid-flow-col gap-4 p-4'>
{Object.entries(expensesData).map(function ([month, data], index) {
return <Card key={index} month={month as keyof Expenses} data={data} expensesLinkData={expensesLinkData} />;
})}
</div>
</div>
);

})}
</div>
</div>
Expand Down
34 changes: 18 additions & 16 deletions pages/finance.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useState } from 'react';

import AsyncAPISummary from '../components/FinancialSummary/AsyncAPISummary';
import BarChartComponent from '../components/FinancialSummary/BarChartComponent';
import ContactUs from '../components/FinancialSummary/ContactUs';
import ExpenseBreakdown from '../components/FinancialSummary/ExpenseBreakdown';
import OtherFormsComponent from '../components/FinancialSummary/OtherFormsComponent';
import SponsorshipTiers from '../components/FinancialSummary/SponsorshipTiers';
import SuccessStories from '../components/FinancialSummary/SuccessStories';
import Container from '../components/layout/Container';

const BarChartComponent = dynamic(() => import('../components/FinancialSummary/BarChartComponent'), { ssr: false });

/**
* @description The FinancialSummary is the financial summary page of the website.
* It contains the AsyncAPI summary, sponsorship tiers, other forms, expense breakdown,
* bar chart, success stories, and contact us components.
*/
export default function FinancialSummary() {
const [mounted, setMounted] = useState(false);
const [windowWidth, setWindowWidth] = useState<number>(0);

const handleResizeRef = useRef<() => void>(null!);

handleResizeRef.current = () => {
setWindowWidth(window.innerWidth);
};

// Handle window resize event to update the window width state value for responsive design purposes
// eslint-disable-next-line consistent-return
useEffect(() => {
handleResizeRef.current!();
window.addEventListener('resize', handleResizeRef.current!);
setMounted(true);
if (typeof window !== 'undefined') {
setWindowWidth(window.innerWidth);
const handleResize = () => {
setWindowWidth(window.innerWidth);
};

window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResizeRef.current!);
};
return () => window.removeEventListener('resize', handleResize);
}
}, []);

const title = 'AsyncAPI Finance Summary';
Expand All @@ -47,13 +49,13 @@ export default function FinancialSummary() {
<SponsorshipTiers />
<OtherFormsComponent />
<ExpenseBreakdown />
<BarChartComponent />
{mounted && <BarChartComponent />}
<SuccessStories />
<ContactUs />
</>
);

const shouldUseContainer = windowWidth > 1700;
const shouldUseContainer = mounted && windowWidth > 1700;

return <div>{shouldUseContainer ? <Container wide>{renderComponents()}</Container> : renderComponents()}</div>;
}
85 changes: 76 additions & 9 deletions scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,82 @@ async function start() {
throw new Error('No finance data found in the finance directory.');
}

const latestYear = yearsList[0];

await buildFinanceInfoList({
currentDir: '.',
configDir: 'config',
financeDir: 'finance',
year: latestYear,
jsonDataDir: 'json-data'
});
// Initialize combined data objects before the year loop
const allExpenses = {};
const allExpensesLinks = [];

// Loop through all years and build finance info list for each year
for (const year of yearsList) {
// Build individual year data
await buildFinanceInfoList({
currentDir: '.',
configDir: 'config',
financeDir: 'finance',
year: year,
jsonDataDir: `json-data/${year}`
});

// Add data to combined collections
const expensesPath = resolve(financeDir, `json-data/${year}/Expenses.json`);
const linksPath = resolve(financeDir, `json-data/${year}/ExpensesLink.json`);

if (fs.existsSync(expensesPath)) {
const yearExpenses = JSON.parse(fs.readFileSync(expensesPath, 'utf8'));
Object.entries(yearExpenses).forEach(([month, expenses]) => {
if (!allExpenses[month]) {
allExpenses[month] = [];
}

// Create a temporary object to hold category sums for this month
const monthCategorySums = {};

// Process existing amounts in allExpenses for this month
allExpenses[month].forEach((existing) => {
monthCategorySums[existing.Category] = parseFloat(existing.Amount);
});

// Add or sum new expenses from current year
expenses.forEach((expense) => {
const amount = parseFloat(expense.Amount);
if (monthCategorySums[expense.Category]) {
monthCategorySums[expense.Category] += amount;
} else {
monthCategorySums[expense.Category] = amount;
}
});

// Convert back to array format with summed amounts
allExpenses[month] = Object.entries(monthCategorySums).map(([Category, Amount]) => ({
Category,
Amount: Amount.toFixed(2)
}));
});
}

if (fs.existsSync(linksPath)) {
const yearLinks = JSON.parse(fs.readFileSync(linksPath, 'utf8'));
yearLinks.forEach(link => {
if (!allExpensesLinks.some(existing => existing.category === link.category)) {
allExpensesLinks.push(link);
}
});
}
}

// Create All_years directory and save combined data
const allYearsDir = resolve(financeDir, 'json-data/All_years');
if (!fs.existsSync(allYearsDir)) {
fs.mkdirSync(allYearsDir, { recursive: true });
}

fs.writeFileSync(
resolve(allYearsDir, 'Expenses.json'),
JSON.stringify(allExpenses)
);
fs.writeFileSync(
resolve(allYearsDir, 'ExpensesLink.json'),
JSON.stringify(allExpensesLinks)
);
}

module.exports = start;
Expand Down
Loading
Loading