mirror of
https://github.com/FranP-code/spend-ia.git
synced 2025-10-13 00:14:09 +00:00
feat: migrated from chart.js to google-charts
This commit is contained in:
4
.babelrc
Normal file
4
.babelrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": [["styled-components", { "ssr": true }]]
|
||||
}
|
||||
@@ -13,7 +13,31 @@ module.exports = {
|
||||
plugins: ['react', 'prettier', 'sort-keys-fix', 'better-styled-components'],
|
||||
rules: {
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
'better-styled-components/sort-declarations-alphabetically': 2,
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'unknown',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index',
|
||||
'object',
|
||||
'type',
|
||||
],
|
||||
pathGroups: [
|
||||
{
|
||||
group: 'external',
|
||||
pattern: '~/**',
|
||||
position: 'after',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'prettier/prettier': 'error',
|
||||
'sort-keys-fix/sort-keys-fix': 'error',
|
||||
},
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ yarn-error.log
|
||||
yarn.lock
|
||||
.next
|
||||
pages/normalize.css
|
||||
pnpm-lock.yaml
|
||||
@@ -1,22 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
|
||||
import { Pie } from 'react-chartjs-2';
|
||||
import Chart from 'react-google-charts';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
import { type Theme } from '@/lib/theme';
|
||||
import { type PieCircleData } from '@/lib/types';
|
||||
|
||||
export const PieCircle = (): JSX.Element => {
|
||||
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||
export const PieCircle = (props: { pieCircleData: PieCircleData }): JSX.Element => {
|
||||
const { pieCircleData } = props;
|
||||
const theme = useTheme() as Theme;
|
||||
const { colors } = theme;
|
||||
const [data, legendData, chartColors] = [
|
||||
pieCircleData.map(([[label, value]]) => [label, parseFloat(value.toFixed(2))]),
|
||||
pieCircleData.map(([, item]) => item),
|
||||
pieCircleData.map(([, { backgroundColor }]) => backgroundColor),
|
||||
];
|
||||
return (
|
||||
<Pie
|
||||
data={{
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: ['rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)'],
|
||||
data: [300, 50, 100],
|
||||
hoverOffset: 4,
|
||||
label: 'My First Dataset',
|
||||
},
|
||||
],
|
||||
labels: ['Red', 'Blue', 'Yellow'],
|
||||
<PieCircleContainer>
|
||||
<StyledChart
|
||||
chartType="PieChart"
|
||||
data={[['X', 'Y'], ...data]}
|
||||
options={{
|
||||
backgroundColor: colors.primary,
|
||||
colors: chartColors,
|
||||
legend: 'none',
|
||||
}}
|
||||
></Pie>
|
||||
width={'100%'}
|
||||
height={'400px'}
|
||||
/>
|
||||
<div>
|
||||
{legendData.map(({ backgroundColor, label }) => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor,
|
||||
borderRadius: '100%',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
}}
|
||||
></div>
|
||||
<span
|
||||
style={{
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</PieCircleContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const PieCircleContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledChart = styled(Chart)`
|
||||
background: 'none';
|
||||
`;
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import { create } from 'zustand';
|
||||
import { type Tab } from '@/lib/types';
|
||||
import { type UserSpendData, type Tab } from '@/lib/types';
|
||||
import { SPEND_SCREEN_ID, SPEND_SCREEN_NAME } from '@/lib/constants';
|
||||
|
||||
interface appStore {
|
||||
tab: Tab;
|
||||
setTab: (props: Tab) => void;
|
||||
setUserSpendData: (props: UserSpendData[]) => void;
|
||||
userSpendData: UserSpendData[];
|
||||
}
|
||||
|
||||
export const useAppStore = create<appStore>((set) => ({
|
||||
setTab: (props: Tab) => {
|
||||
set(() => ({ tab: props }));
|
||||
},
|
||||
setUserSpendData: (props: UserSpendData[]) => {
|
||||
set(() => ({ userSpendData: props }));
|
||||
},
|
||||
tab: { id: SPEND_SCREEN_ID, title: SPEND_SCREEN_NAME },
|
||||
userSpendData: [],
|
||||
}));
|
||||
|
||||
24
lib/theme.ts
Normal file
24
lib/theme.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
export interface Theme {
|
||||
colors: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
complementary: string;
|
||||
textColor: {
|
||||
primary: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const theme: Theme = {
|
||||
colors: {
|
||||
primary: '#635985',
|
||||
secondary: '#443C68',
|
||||
complementary: '#393053',
|
||||
textColor: {
|
||||
primary: '#ddd',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default theme;
|
||||
21
lib/types.d.ts
vendored
21
lib/types.d.ts
vendored
@@ -1,4 +1,23 @@
|
||||
export interface Tab {
|
||||
title: string;
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Currency {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
label: string;
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
export interface UserSpendData {
|
||||
category: Category;
|
||||
currency: Currency;
|
||||
date: Date;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export type PieCircleData = Array<[[string, number], { backgroundColor: string; label: string }]>;
|
||||
|
||||
37
package.json
37
package.json
@@ -16,11 +16,11 @@
|
||||
"husky-prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"chart.js": "^4.2.1",
|
||||
"next": "^13.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-google-charts": "^4.0.0",
|
||||
"styled-components": "^5.3.9",
|
||||
"zustand": "^4.3.7"
|
||||
},
|
||||
@@ -31,6 +31,7 @@
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-standard-with-typescript": "^34.0.1",
|
||||
@@ -51,10 +52,42 @@
|
||||
"stylelint-order": "^6.0.3",
|
||||
"stylelint-prettier": "^3.0.0",
|
||||
"stylelint-processor-styled-components": "^1.10.0",
|
||||
"typescript": "*",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"styled-components": "^5"
|
||||
},
|
||||
"babel": {
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,3 +26,7 @@ a:hover {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#__next, body, html {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import { Header, SpendScreen } from '@/screens';
|
||||
import { type Tab } from '@/lib/types';
|
||||
import { APP_NAME, SPEND_SCREEN_ID } from '@/lib/constants';
|
||||
import Head from 'next/head';
|
||||
import { useAppStore } from '@/lib/storage';
|
||||
import theme from '@/lib/theme';
|
||||
|
||||
const HeadIndex = (): JSX.Element => {
|
||||
return (
|
||||
const HeadIndex = (): JSX.Element => (
|
||||
<>
|
||||
<Head>
|
||||
<title>{APP_NAME}</title>
|
||||
<meta property="og:title" content={APP_NAME} key="title" />
|
||||
</Head>
|
||||
</>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
const appRender = ({ tab }: { tab: Tab }): JSX.Element => {
|
||||
switch (tab.id) {
|
||||
@@ -27,13 +27,56 @@ const appRender = ({ tab }: { tab: Tab }): JSX.Element => {
|
||||
|
||||
function App(): JSX.Element {
|
||||
const tab = useAppStore((state) => state.tab);
|
||||
const setUserSpendData = useAppStore((state) => state.setUserSpendData);
|
||||
setUserSpendData([
|
||||
{
|
||||
category: { backgroundColor: 'rgb(99, 128, 255)', label: 'invest' },
|
||||
currency: {
|
||||
id: 'usd',
|
||||
label: 'usd',
|
||||
},
|
||||
date: new Date(),
|
||||
value: 124,
|
||||
},
|
||||
{
|
||||
category: { backgroundColor: 'rgb(99, 128, 255)', label: 'invest' },
|
||||
currency: {
|
||||
id: 'usd',
|
||||
label: 'usd',
|
||||
},
|
||||
date: new Date(),
|
||||
value: 124.1,
|
||||
},
|
||||
{
|
||||
category: { backgroundColor: 'rgb(54, 162, 235)', label: 'school' },
|
||||
currency: {
|
||||
id: 'usd',
|
||||
label: 'usd',
|
||||
},
|
||||
date: new Date(),
|
||||
value: 124.43335,
|
||||
},
|
||||
{
|
||||
category: { backgroundColor: 'rgb(145, 86, 255)', label: 'party' },
|
||||
currency: {
|
||||
id: 'usd',
|
||||
label: 'usd',
|
||||
},
|
||||
date: new Date(),
|
||||
value: 1242,
|
||||
},
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<ThemeProvider theme={theme}>
|
||||
<HeadIndex />
|
||||
<Header />
|
||||
{appRender({ tab })}
|
||||
</>
|
||||
<Body>{appRender({ tab })}</Body>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const Body = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useAppStore } from '@/lib/storage';
|
||||
import { type Theme } from '@/lib/theme';
|
||||
import { tabs } from './data';
|
||||
|
||||
export const Header = (): JSX.Element => {
|
||||
@@ -17,7 +18,7 @@ export const Header = (): JSX.Element => {
|
||||
setTab(tabData);
|
||||
}}
|
||||
>
|
||||
<h3>{tabData.title}</h3>
|
||||
<TabText>{tabData.title}</TabText>
|
||||
</StyledTab>
|
||||
))}
|
||||
</TabsContainer>
|
||||
@@ -25,14 +26,15 @@ export const Header = (): JSX.Element => {
|
||||
};
|
||||
|
||||
const TabsContainer = styled.div`
|
||||
background: #635985;
|
||||
background: ${({ theme }) => theme.colors.secondary};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledTab = styled.div<{
|
||||
active: boolean;
|
||||
}>`
|
||||
background: ${({ active }) => (active ? '#443C68' : '#635985')};
|
||||
background: ${({ active, theme }: { active: boolean; theme: Theme }) =>
|
||||
active ? theme.colors.complementary : theme.colors.secondary};
|
||||
padding: 12px 0px;
|
||||
text-align: center;
|
||||
transition: 0.2s ease-in-out all;
|
||||
@@ -43,3 +45,7 @@ const StyledTab = styled.div<{
|
||||
transition: 0.2s ease-in-out all;
|
||||
}
|
||||
`;
|
||||
|
||||
const TabText = styled.h3`
|
||||
color: ${({ theme }: { theme: Theme }) => theme.colors.textColor.primary};
|
||||
`;
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { PieCircle } from '@/components';
|
||||
import { type Theme } from '@/lib/theme';
|
||||
import { useAppStore } from '@/lib/storage';
|
||||
import { type PieCircleData } from '@/lib/types';
|
||||
|
||||
export const SpendScreen = (): JSX.Element => {
|
||||
const userSpendData = useAppStore((state) => state.userSpendData);
|
||||
const reducedUserData = userSpendData.reduce(
|
||||
(acc, value) =>
|
||||
acc.set(value.category.label, [...(acc.get(value.category.label) || []), value]),
|
||||
new Map(),
|
||||
);
|
||||
const combinedUserData: PieCircleData = [...reducedUserData.entries()].map(([key, values]) => [
|
||||
[key, values.reduce((acc: number, { value }: { value: number }) => acc + value, 0)],
|
||||
{ backgroundColor: values[0].category.backgroundColor, label: values[0].category.label },
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<PieCircle />
|
||||
</>
|
||||
<SpendScreenContainer>
|
||||
<PieCircle pieCircleData={combinedUserData} />
|
||||
</SpendScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const SpendScreenContainer = styled.div`
|
||||
background-color: ${({ theme }: { theme: Theme }) => theme.colors.primary};
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user