feat: migrated from chart.js to google-charts

This commit is contained in:
2023-06-10 21:17:16 -03:00
parent e7149f53ff
commit db0d493944
13 changed files with 265 additions and 42 deletions

4
.babelrc Normal file
View File

@@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": [["styled-components", { "ssr": true }]]
}

View File

@@ -13,7 +13,31 @@ module.exports = {
plugins: ['react', 'prettier', 'sort-keys-fix', 'better-styled-components'], plugins: ['react', 'prettier', 'sort-keys-fix', 'better-styled-components'],
rules: { rules: {
'@typescript-eslint/strict-boolean-expressions': 'off', '@typescript-eslint/strict-boolean-expressions': 'off',
'arrow-body-style': ['error', 'as-needed'],
'better-styled-components/sort-declarations-alphabetically': 2, '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', 'prettier/prettier': 'error',
'sort-keys-fix/sort-keys-fix': 'error', 'sort-keys-fix/sort-keys-fix': 'error',
}, },

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ yarn-error.log
yarn.lock yarn.lock
.next .next
pages/normalize.css pages/normalize.css
pnpm-lock.yaml

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
node

View File

@@ -1,22 +1,61 @@
import React from 'react'; import React from 'react';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'; import Chart from 'react-google-charts';
import { Pie } from 'react-chartjs-2'; import styled, { useTheme } from 'styled-components';
import { type Theme } from '@/lib/theme';
import { type PieCircleData } from '@/lib/types';
export const PieCircle = (): JSX.Element => { export const PieCircle = (props: { pieCircleData: PieCircleData }): JSX.Element => {
ChartJS.register(ArcElement, Tooltip, Legend); 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 ( return (
<Pie <PieCircleContainer>
data={{ <StyledChart
datasets: [ chartType="PieChart"
{ data={[['X', 'Y'], ...data]}
backgroundColor: ['rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)'], options={{
data: [300, 50, 100], backgroundColor: colors.primary,
hoverOffset: 4, colors: chartColors,
label: 'My First Dataset', legend: 'none',
},
],
labels: ['Red', 'Blue', 'Yellow'],
}} }}
></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';
`;

View File

@@ -1,16 +1,22 @@
/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/ban-types */
import { create } from 'zustand'; 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'; import { SPEND_SCREEN_ID, SPEND_SCREEN_NAME } from '@/lib/constants';
interface appStore { interface appStore {
tab: Tab; tab: Tab;
setTab: (props: Tab) => void; setTab: (props: Tab) => void;
setUserSpendData: (props: UserSpendData[]) => void;
userSpendData: UserSpendData[];
} }
export const useAppStore = create<appStore>((set) => ({ export const useAppStore = create<appStore>((set) => ({
setTab: (props: Tab) => { setTab: (props: Tab) => {
set(() => ({ tab: props })); set(() => ({ tab: props }));
}, },
setUserSpendData: (props: UserSpendData[]) => {
set(() => ({ userSpendData: props }));
},
tab: { id: SPEND_SCREEN_ID, title: SPEND_SCREEN_NAME }, tab: { id: SPEND_SCREEN_ID, title: SPEND_SCREEN_NAME },
userSpendData: [],
})); }));

24
lib/theme.ts Normal file
View 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
View File

@@ -1,4 +1,23 @@
export interface Tab { export interface Tab {
title: string;
id: 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 }]>;

View File

@@ -16,11 +16,11 @@
"husky-prepare": "husky install" "husky-prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"chart.js": "^4.2.1",
"next": "^13.2.4", "next": "^13.2.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-google-charts": "^4.0.0",
"styled-components": "^5.3.9", "styled-components": "^5.3.9",
"zustand": "^4.3.7" "zustand": "^4.3.7"
}, },
@@ -31,6 +31,7 @@
"@types/styled-components": "^5.1.26", "@types/styled-components": "^5.1.26",
"@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/eslint-plugin": "^5.43.0",
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^3.1.0",
"babel-plugin-styled-components": "^2.0.7",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-config-standard-with-typescript": "^34.0.1", "eslint-config-standard-with-typescript": "^34.0.1",
@@ -51,10 +52,42 @@
"stylelint-order": "^6.0.3", "stylelint-order": "^6.0.3",
"stylelint-prettier": "^3.0.0", "stylelint-prettier": "^3.0.0",
"stylelint-processor-styled-components": "^1.10.0", "stylelint-processor-styled-components": "^1.10.0",
"typescript": "*", "typescript": "^5.0.4",
"vite": "^4.2.0" "vite": "^4.2.0"
}, },
"resolutions": { "resolutions": {
"styled-components": "^5" "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
}
]
]
}
}
} }
} }

View File

@@ -26,3 +26,7 @@ a:hover {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
#__next, body, html {
height: 100%;
}

View File

@@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import Head from 'next/head';
import styled, { ThemeProvider } from 'styled-components';
import { Header, SpendScreen } from '@/screens'; import { Header, SpendScreen } from '@/screens';
import { type Tab } from '@/lib/types'; import { type Tab } from '@/lib/types';
import { APP_NAME, SPEND_SCREEN_ID } from '@/lib/constants'; import { APP_NAME, SPEND_SCREEN_ID } from '@/lib/constants';
import Head from 'next/head';
import { useAppStore } from '@/lib/storage'; import { useAppStore } from '@/lib/storage';
import theme from '@/lib/theme';
const HeadIndex = (): JSX.Element => { const HeadIndex = (): JSX.Element => (
return (
<> <>
<Head> <Head>
<title>{APP_NAME}</title> <title>{APP_NAME}</title>
@@ -14,7 +15,6 @@ const HeadIndex = (): JSX.Element => {
</Head> </Head>
</> </>
); );
};
const appRender = ({ tab }: { tab: Tab }): JSX.Element => { const appRender = ({ tab }: { tab: Tab }): JSX.Element => {
switch (tab.id) { switch (tab.id) {
@@ -27,13 +27,56 @@ const appRender = ({ tab }: { tab: Tab }): JSX.Element => {
function App(): JSX.Element { function App(): JSX.Element {
const tab = useAppStore((state) => state.tab); 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 ( return (
<> <ThemeProvider theme={theme}>
<HeadIndex /> <HeadIndex />
<Header /> <Header />
{appRender({ tab })} <Body>{appRender({ tab })}</Body>
</> </ThemeProvider>
); );
} }
const Body = styled.div`
height: 100%;
`;
export default App; export default App;

View File

@@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { useAppStore } from '@/lib/storage'; import { useAppStore } from '@/lib/storage';
import { type Theme } from '@/lib/theme';
import { tabs } from './data'; import { tabs } from './data';
export const Header = (): JSX.Element => { export const Header = (): JSX.Element => {
@@ -17,7 +18,7 @@ export const Header = (): JSX.Element => {
setTab(tabData); setTab(tabData);
}} }}
> >
<h3>{tabData.title}</h3> <TabText>{tabData.title}</TabText>
</StyledTab> </StyledTab>
))} ))}
</TabsContainer> </TabsContainer>
@@ -25,14 +26,15 @@ export const Header = (): JSX.Element => {
}; };
const TabsContainer = styled.div` const TabsContainer = styled.div`
background: #635985; background: ${({ theme }) => theme.colors.secondary};
display: flex; display: flex;
`; `;
const StyledTab = styled.div<{ const StyledTab = styled.div<{
active: boolean; active: boolean;
}>` }>`
background: ${({ active }) => (active ? '#443C68' : '#635985')}; background: ${({ active, theme }: { active: boolean; theme: Theme }) =>
active ? theme.colors.complementary : theme.colors.secondary};
padding: 12px 0px; padding: 12px 0px;
text-align: center; text-align: center;
transition: 0.2s ease-in-out all; transition: 0.2s ease-in-out all;
@@ -43,3 +45,7 @@ const StyledTab = styled.div<{
transition: 0.2s ease-in-out all; transition: 0.2s ease-in-out all;
} }
`; `;
const TabText = styled.h3`
color: ${({ theme }: { theme: Theme }) => theme.colors.textColor.primary};
`;

View File

@@ -1,10 +1,29 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import { PieCircle } from '@/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 => { 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 ( return (
<> <SpendScreenContainer>
<PieCircle /> <PieCircle pieCircleData={combinedUserData} />
</> </SpendScreenContainer>
); );
}; };
const SpendScreenContainer = styled.div`
background-color: ${({ theme }: { theme: Theme }) => theme.colors.primary};
height: 100%;
`;