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'],
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',
},

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ package-lock.json
yarn-error.log
yarn.lock
.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 { 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'],
}}
></Pie>
<PieCircleContainer>
<StyledChart
chartType="PieChart"
data={[['X', 'Y'], ...data]}
options={{
backgroundColor: colors.primary,
colors: chartColors,
legend: 'none',
}}
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 */
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
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 {
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 }]>;

View File

@@ -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
}
]
]
}
}
}
}

View File

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

View File

@@ -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 (
<>
<Head>
<title>{APP_NAME}</title>
<meta property="og:title" content={APP_NAME} key="title" />
</Head>
</>
);
};
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;

View File

@@ -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};
`;

View File

@@ -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%;
`;