Build a Finance SaaS Platform -25 (Spending Pie)

Add Spending Pie Component

Add components/spending-pie.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

import { useState } from "react";
import { FileSearch, Loader2, PieChart, Radar, Target } from "lucide-react";

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"

import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";

import { PieVariant } from "@/components/pie-variant";
import { RadialVariant } from "@/components/radial-variant";
import { RadarVariant } from "./radar-variant";
import { Skeleton } from "./ui/skeleton";


type Props = {
data?: {
name: string;
value: number;
}[];
};

export const SpendingPie = ( { data = []} : Props) => {
const [chartType, setChartType] = useState("pie");

const onTypeChange = ( type: string) => {
// TODO: Add paywall
setChartType(type);
}
return(
<Card className="border-none drop-shadow-sm">
<CardHeader className="flex space-y-2 lg:space-y-0 lg:flex-row lg:items-center justify-between">
<CardTitle className="text-xl line-clamp-1">
Categories
</CardTitle>
<Select
defaultValue={chartType}
onValueChange={onTypeChange}
>
<SelectTrigger className="lg:w-auto h-9 rounded-md px-3">
<SelectValue placeholder="Chart type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pie">
<div className="flex items-center">
<PieChart className="size-4 mr-2 shrink-0" />
<p className="line-clamp-1">
Pie chart
</p>
</div>
</SelectItem>
<SelectItem value="radar">
<div className="flex items-center">
<Radar className="size-4 mr-2 shrink-0" />
<p className="line-clamp-1">
Radar chart
</p>
</div>
</SelectItem>
<SelectItem value="radial">
<div className="flex items-center">
<Target className="size-4 mr-2 shrink-0" />
<p className="line-clamp-1">
Radial chart
</p>
</div>
</SelectItem>
</SelectContent>
</Select>
</CardHeader>
<CardContent>
{data.length === 0 ? (
<div className="felx flex-col gap-y-4 items-center justify-center h-[350px] w-full">
<FileSearch className="size-6 text-muted-foreground" />
<p className="text-muted-foreground text-sm">
No data for this period
</p>
</div>
) : (
<>
{ chartType === "pie" && <PieVariant data={data} /> }
{ chartType === "radar" && <RadarVariant data={data} /> }
{ chartType === "radial" && <RadialVariant data={data} /> }
</>
)}
</CardContent>
</Card>
)
}

export const SpendingPieLoading =() => {
return (
<Card className="border-none drop-shadow-sm">
<CardHeader className="flex space-y-2 lg:space-y-0 lg:flex-row lg:items-center justify-between">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-8 lg:w-[120px] w-full" />
</CardHeader>
<CardContent>
<div className="h-[350px] w-full flex items-center justify-center">
<Loader2 className="h-6 w-6 text-slate-300 animate-spin"/>
</div>
</CardContent>
</Card>
);
};

Add Pie Variant Component

Add components/pie-variant.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { formatPercentage } from "@/lib/utils";
import {
Cell,
Legend,
Pie,
PieChart,
ResponsiveContainer,
Tooltip
} from "recharts"
import { CategoryTooltip } from "./category-tooltip";

const COLORS = ["#0062FF","#12C6FF","#FF64FF","#FF9354"];
type Props = {
data?: {
name: string;
value: number;
}[];
};

export const PieVariant = ({ data }: Props) => {
return (
<ResponsiveContainer width="100%" height={350}>
<PieChart>
<Legend
layout="horizontal"
verticalAlign="bottom"
align="right"
iconType="circle"
content={({ payload}:any) =>{
return(
<ul className="flex flex-col space-y-2">
{payload.map((entry: any, index:number) =>(
<li
key={`item-${index}`}
className="flex items-center space-x-2"
>
<span
className="size-2 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<div className="space-x-1">
<span className="text-sm text-muted-foreground">
{entry.value}
</span>
<span className="text-sm">
{formatPercentage(entry.payload.percent * 100)}
</span>
</div>
</li>
))}
</ul>
)
}}
/>
<Tooltip content={<CategoryTooltip />} />
<Pie
data={data}
cx="50%"
cy="50%"
outerRadius={90}
innerRadius={60}
paddingAngle={2}
fill="#8884d8"
dataKey="value"
labelLine={false}
>
{data?.map((_entry,index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
);
};

Add Bar Variant Component

Add components/radar-variant.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

import {
PolarAngleAxis,
PolarGrid,
PolarRadiusAxis,
Radar,
RadarChart,
ResponsiveContainer
} from "recharts"

type Props = {
data?: {
name: string;
value: number;
}[];
};

export const RadarVariant = ({ data }: Props) => {
return (
<ResponsiveContainer width="100%" height={350}>
<RadarChart
cx="50%"
cy="50%"
outerRadius="60%"
data={data}
>
<PolarGrid />
<PolarAngleAxis style={{ fontSize: "12px" }} dataKey="name" />
<PolarRadiusAxis style={{ fontSize: "12px" }} />
<Radar dataKey="value" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.6} />
</RadarChart>
</ResponsiveContainer>
);
};

Add Radial Component

Add components/radial-variant.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { formatCurrency } from "@/lib/utils";
import {
RadialBar,
RadialBarChart,
Legend,
ResponsiveContainer,
Tooltip
} from "recharts"

const COLORS = ["#0062FF","#12C6FF","#FF64FF","#FF9354"];
type Props = {
data?: {
name: string;
value: number;
}[];
};

export const RadialVariant = ({ data }: Props) => {
return (
<ResponsiveContainer width="100%" height={350}>
<RadialBarChart
cx="50$"
cy="30%"
barSize={10}
innerRadius="90%"
outerRadius="40%"
data={data?.map((item,index) =>({
...item,
fill: COLORS[index % COLORS.length]
}))}
>
<RadialBar
label={{
position:"insideStart",
fill:"#fff",
fontSize: "12px",
}}
background
dataKey="value"
/>
<Legend
layout="horizontal"
verticalAlign="bottom"
align="right"
iconType="circle"
content={({ payload}:any) =>{
return(
<ul className="flex flex-col space-y-2">
{payload.map((entry: any, index:number) =>(
<li
key={`item-${index}`}
className="flex items-center space-x-2"
>
<span
className="size-2 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<div className="space-x-1">
<span className="text-sm text-muted-foreground">
{entry.value}
</span>
<span className="text-sm">
{formatCurrency(entry.payload.value)}
</span>
</div>
</li>
))}
</ul>
)
}}
/>
</RadialBarChart>
</ResponsiveContainer>
);
};

Add Category Tooltip

Add components/category-tooltip.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Separator } from "./ui/separator";
import { formatCurrency } from "@/lib/utils";

export const CategoryTooltip = ( { active, payload } : any ) => {
if (!active) return null;

const name = payload[0].payload.name;
const value = payload[0].value;

return(
<div className="rounded-sm bg-white shadow-sm border overflow-hidden">
<div className="text-sm p-2 px-3 bg-muted text-muted-foreground">
{name}
</div>
<Separator />
<div className="p-2 px-3 space-y-1">
<div className="flex items-center justify-between gap-x-4">
<div className="flex items-center gap-x-2">
<div className="size-1.5 bg-rose-500 rounded-full" />
<p className="text-sm text-muted-foreground">
Expenses
</p>
</div>
<p className="text-sm text-right font-medium">
{formatCurrency(value * -1)}
</p>
</div>
</div>
</div>
)
}

Modify Chart, Add Loading

Modify components/chart.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
....
export const ChartLoading =() => {
return (
<Card className="border-none drop-shadow-sm">
<CardHeader className="flex space-y-2 lg:space-y-0 lg:flex-row lg:items-center justify-between">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-8 lg:w-[120px] w-full" />
</CardHeader>
<CardContent>
<div className="h-[350px] w-full flex items-center justify-center">
<Loader2 className="h-6 w-6 text-slate-300 animate-spin"/>
</div>
</CardContent>
</Card>
);
};

Modify Data Chart, Add Spending Pie and Loading

Modify components/data-charts.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"use client";

import { useGetSummary } from "@/features/summary/api/use-get-summary";
import { Chart, ChartLoading } from "@/components/chart";
import { SpendingPie, SpendingPieLoading } from "@/components/spending-pie";

export const DataCharts = () => {
const { data, isLoading} = useGetSummary();

if( isLoading) {
return(
<div className="grid grid-cols-1 lg:grid-cols-6 gap-8">
<div className="col-span-1 lg:col-span-3 xl:col-span-4">
<ChartLoading />
</div>
<div className="col-span-1 lg:col-span-3 xl:col-span-2">
<SpendingPieLoading />
</div>
</div>
)
}

return(
<div className="grid grid-cols-1 lg:grid-cols-6 gap-8">
<div className="col-span-1 lg:col-span-3 xl:col-span-4">
<Chart data={data?.days} />
</div>
<div className="col-span-1 lg:col-span-3 xl:col-span-2">
<SpendingPie data={data?.categories} />
</div>
</div>
);
}

请我喝杯咖啡吧~

支付宝
微信