Skip to content

Commit 542781f

Browse files
authored
Feat/custom splits (#8)
* feat: custom splits form, update split card * feat: editing existing custom splits * fix: revert unique userId * feat: money flow bar chart, fix seed
1 parent c97fcba commit 542781f

File tree

18 files changed

+621
-61
lines changed

18 files changed

+621
-61
lines changed

components/MoneySplitForm.tsx

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { useFormikContext } from "formik";
2+
import { useSession } from "next-auth/react";
3+
import React, { Dispatch, ReactNode, SetStateAction, useState } from "react";
4+
import useSWR from "swr";
5+
import { GroupWithUsers } from "types/groups";
6+
import { Avatar } from "./common/Avatar/Avatar";
7+
import { Expense } from "./forms/ExpenseForm";
8+
9+
export type Label = {
10+
id: string;
11+
title: string;
12+
icon: ReactNode;
13+
};
14+
15+
export type Budgets = {
16+
[key: string]: number | undefined | null;
17+
};
18+
19+
type MoneySplitFormProps = {
20+
limitAmount: number;
21+
state: Budgets;
22+
setState: (field: string, value: any, shouldValidate?: boolean) => void;
23+
};
24+
25+
export const MoneySplitForm = ({
26+
limitAmount,
27+
state,
28+
setState,
29+
}: MoneySplitFormProps) => {
30+
const { values } = useFormikContext<Expense>();
31+
const { data: session } = useSession();
32+
33+
const { data: selectedGroup } = useSWR<GroupWithUsers>(
34+
`/api/group/${values.group}`
35+
);
36+
37+
const splitBetween =
38+
selectedGroup &&
39+
selectedGroup?.users.map((user) => {
40+
return {
41+
id: user.userId,
42+
title:
43+
user.userId === session?.user.id
44+
? "You"
45+
: (user.user.name as string),
46+
icon: <Avatar imgSrc={user.user.image || ""} />,
47+
};
48+
});
49+
50+
const handleBudgetChange = (categoryId: string, value: number) => {
51+
const newValue = isNaN(value) ? null : value;
52+
const newBudgets = { ...state, [categoryId]: newValue };
53+
const totalBudget = Object.values(newBudgets).reduce(
54+
(acc: number, cur) => (cur ? acc + cur : acc),
55+
0
56+
);
57+
58+
if (totalBudget <= limitAmount) {
59+
setState("budget", newBudgets);
60+
}
61+
};
62+
63+
return (
64+
<div className="text-white">
65+
<div className="flex flex-col gap-4">
66+
{splitBetween &&
67+
splitBetween.map((item) => (
68+
<div key={item.id} className="flex gap-4">
69+
{item.icon}
70+
<div className="flex w-full flex-col gap-1">
71+
<div className="flex justify-between">
72+
<label htmlFor={item.id}>
73+
{item.title} (
74+
{limitAmount &&
75+
(
76+
((state[item.id] ?? 0) /
77+
limitAmount) *
78+
100
79+
).toFixed(0)}
80+
%)
81+
</label>
82+
<input
83+
type="number"
84+
max={limitAmount}
85+
value={state[item.id]}
86+
disabled={!limitAmount}
87+
onChange={(e) =>
88+
handleBudgetChange(
89+
item.id,
90+
parseFloat(e.target.value)
91+
)
92+
}
93+
className="mt-1 w-20 text-black"
94+
/>
95+
</div>
96+
<input
97+
type="range"
98+
max={limitAmount}
99+
step={0.01}
100+
disabled={!limitAmount}
101+
value={state[item.id]}
102+
onChange={(e) =>
103+
handleBudgetChange(
104+
item.id,
105+
parseFloat(e.target.value)
106+
)
107+
}
108+
/>
109+
</div>
110+
</div>
111+
))}
112+
</div>
113+
</div>
114+
);
115+
};

components/activity/SplitCard.tsx

+58-20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Icon } from "@iconify/react";
12
import { useSession } from "next-auth/react";
23
import Link from "next/link";
34
import { FunctionComponent } from "react";
@@ -9,7 +10,7 @@ type SplitCardProps = {
910
};
1011

1112
export const SplitCard: FunctionComponent<SplitCardProps> = ({ activity }) => {
12-
const { title, currentUserBill, Bill } = activity;
13+
const { title, currentUserBill, Bill, amount, isSplit } = activity;
1314

1415
const { data: session } = useSession();
1516

@@ -21,6 +22,8 @@ export const SplitCard: FunctionComponent<SplitCardProps> = ({ activity }) => {
2122
}
2223
}, 0);
2324

25+
const moneyUndecided = amount - moneyLent;
26+
2427
const moneyOwed = Bill.reduce((total, item) => {
2528
if (item.userId === session?.user?.id && item.hasParticipated) {
2629
return total + (item.amount || 0);
@@ -33,19 +36,40 @@ export const SplitCard: FunctionComponent<SplitCardProps> = ({ activity }) => {
3336
<Link href={`/expense/${activity.id}`}>
3437
<div className="flex items-center justify-between rounded-sm bg-black/80 px-4 py-3 text-neutral-300 ">
3538
<div className="flex w-full items-center justify-between">
36-
<div>
37-
<p className="text-lg font-medium">{title}</p>
38-
<p>
39-
<span className="font-medium">
40-
{activity.user.id === session?.user.id
41-
? "You"
42-
: activity.user.name}
43-
</span>{" "}
44-
paid{" "}
45-
<span className="font-medium">
46-
{amountFormatter(activity.amount)} PLN
47-
</span>
48-
</p>
39+
<div className="flex items-center gap-6">
40+
<div className="flex flex-col items-center justify-center text-xs text-neutral-500 min-w-[40px]">
41+
{isSplit ? (
42+
<>
43+
<Icon
44+
icon="ic:sharp-grid-view"
45+
className="h-6 w-6"
46+
/>
47+
<span>Evenly</span>
48+
</>
49+
) : (
50+
<>
51+
<Icon
52+
icon="material-symbols:space-dashboard-sharp"
53+
className="h-6 w-6"
54+
/>
55+
<span>Custom</span>
56+
</>
57+
)}
58+
</div>
59+
<div>
60+
<p className="text-lg font-medium">{title}</p>
61+
<p>
62+
<span className="font-medium">
63+
{activity.user.id === session?.user.id
64+
? "You"
65+
: activity.user.name}
66+
</span>{" "}
67+
paid{" "}
68+
<span className="font-medium">
69+
{amountFormatter(activity.amount)} PLN
70+
</span>
71+
</p>
72+
</div>
4973
</div>
5074
<>
5175
{!currentUserBill ? (
@@ -67,12 +91,25 @@ export const SplitCard: FunctionComponent<SplitCardProps> = ({ activity }) => {
6791
No balance
6892
</span>
6993
) : (
70-
<span className="font-medium text-green-500">
71-
You lent{" "}
72-
{amountFormatter(
73-
moneyLent
94+
<div className="flex flex-col">
95+
<span className="font-medium text-green-500">
96+
You lent{" "}
97+
{amountFormatter(
98+
moneyLent
99+
)}{" "}
100+
PLN
101+
</span>
102+
{moneyUndecided > 0 && (
103+
<span className="text-xs">
104+
(
105+
{amountFormatter(
106+
moneyUndecided
107+
)}{" "}
108+
PLN left to
109+
split)
110+
</span>
74111
)}
75-
</span>
112+
</div>
76113
)}
77114
</>
78115
) : (
@@ -86,7 +123,8 @@ export const SplitCard: FunctionComponent<SplitCardProps> = ({ activity }) => {
86123
You owe{" "}
87124
{amountFormatter(
88125
moneyOwed
89-
)}
126+
)}{" "}
127+
PLN
90128
</span>
91129
)}
92130
</>

components/charts/MoneyFlowChart.tsx

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from "react";
2+
import {
3+
BarChart,
4+
Bar,
5+
XAxis,
6+
YAxis,
7+
CartesianGrid,
8+
Tooltip,
9+
Legend,
10+
ResponsiveContainer,
11+
} from "recharts";
12+
import { amountFormatter } from "utils/formatters";
13+
14+
interface IncomeExpensePeriod {
15+
startDate: string;
16+
endDate: string;
17+
income: number;
18+
expense: number;
19+
}
20+
21+
interface IncomeExpenseBarChartProps {
22+
data: IncomeExpensePeriod[];
23+
}
24+
25+
const IncomeExpenseBarChart: React.FC<IncomeExpenseBarChartProps> = ({
26+
data,
27+
}) => {
28+
const formattedData = data.map((item, index) => {
29+
return {
30+
period: index === 0 ? "Base" : "Compare",
31+
income: item.income,
32+
expense: item.expense,
33+
};
34+
});
35+
36+
return (
37+
<div className="flex items-center">
38+
<ResponsiveContainer width="40%" height={300}>
39+
<BarChart
40+
width={600}
41+
height={300}
42+
data={formattedData}
43+
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
44+
>
45+
<CartesianGrid strokeDasharray="3 3" />
46+
<XAxis dataKey="period" />
47+
<YAxis />
48+
<Tooltip />
49+
<Legend />
50+
<Bar dataKey="income" fill="#8884d8" />
51+
<Bar dataKey="expense" fill="#82ca9d" />
52+
</BarChart>
53+
</ResponsiveContainer>
54+
<div className="text-white flex-col flex gap-6">
55+
{formattedData.map((item) => (
56+
<div key={item.period} className="flex flex-col">
57+
<span className="text-neutral-300">{item.period}</span>
58+
<span>Income: {amountFormatter(item.income)} PLN</span>
59+
<span>Expenses: {amountFormatter(item.expense)} PLN</span>
60+
</div>
61+
))}
62+
</div>
63+
</div>
64+
);
65+
};
66+
67+
export default IncomeExpenseBarChart;

components/common/Avatar/Avatar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type AvatarProps = {
99
export const Avatar: FunctionComponent<AvatarProps> = ({ imgSrc, imgAlt }) => {
1010
return (
1111
<div
12-
className={`relative h-16 w-16 overflow-hidden rounded-full ring-2 ring-white dark:ring-neutral-900`}
12+
className={`relative h-16 w-16 overflow-hidden rounded-full ring-2 ring-white dark:ring-neutral-900 flex-shrink-0`}
1313
>
1414
<Image src={imgSrc} alt={imgAlt || ""} fill />
1515
</div>

0 commit comments

Comments
 (0)