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'],
|
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',
|
||||||
},
|
},
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ package-lock.json
|
|||||||
yarn-error.log
|
yarn-error.log
|
||||||
yarn.lock
|
yarn.lock
|
||||||
.next
|
.next
|
||||||
pages/normalize.css
|
pages/normalize.css
|
||||||
|
pnpm-lock.yaml
|
||||||
@@ -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',
|
||||||
},
|
}}
|
||||||
],
|
width={'100%'}
|
||||||
labels: ['Red', 'Blue', 'Yellow'],
|
height={'400px'}
|
||||||
}}
|
/>
|
||||||
></Pie>
|
<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 */
|
/* 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
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 {
|
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 }]>;
|
||||||
|
|||||||
37
package.json
37
package.json
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,3 +26,7 @@ a:hover {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#__next, body, html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
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>
|
<meta property="og:title" content={APP_NAME} key="title" />
|
||||||
<meta property="og:title" content={APP_NAME} key="title" />
|
</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;
|
||||||
|
|||||||
@@ -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};
|
||||||
|
`;
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user