diff --git a/src/app/_components/expense/create-expense-page.tsx b/src/app/_components/expense/create-expense-page.tsx
new file mode 100644
index 0000000..b9a7ea3
--- /dev/null
+++ b/src/app/_components/expense/create-expense-page.tsx
@@ -0,0 +1,57 @@
+"use client";
+import React from "react";
+import ExpenseForm from "./expense-form";
+import Header from "@/components/header";
+import { Button } from "@/components/ui/button";
+import type { User } from "@/server/db/schema";
+import { useExpenseStore } from "@/lib/store/expense-store";
+import ExpenseSplit from "./expense-split";
+import ExpenseParticipants from "./expense-participants";
+import { Separator } from "@/components/ui/separator";
+import { useSearchParams } from "next/navigation";
+import { api } from "@/trpc/react";
+
+function ExpensePage({ sessionUser }: { sessionUser: User }) {
+ const addParticipants = useExpenseStore((state) => state.addParticipants);
+ const setPayments = useExpenseStore((state) => state.setPayments);
+ React.useEffect(() => {
+ addParticipants([sessionUser]);
+ setPayments([{ amount: 0, userId: sessionUser.id }]);
+ }, []);
+
+ const groupBadges = useExpenseStore((state) => state.groupBadges);
+ const addGroupBadges = useExpenseStore((state) => state.addGroupBadges);
+
+ const searchParams = useSearchParams();
+ const paramGroup = searchParams.get("groupId");
+ const paramUser = searchParams.get("userId");
+ // React.useEffect(() => {
+ // console.log("Params: ", paramGroup, paramUser);
+ // if (paramGroup?.length) {
+ // if (!groupBadges.find((group) => group.id === paramGroup)) {
+ // const { data: group } = api.group.get.useQuery({
+ // id: paramGroup,
+ // });
+ // if (group) addGroupBadges([group]);
+ // if (group?.members?.length)
+ // addParticipants(group.members.map(({ user }) => user));
+ // }
+ // }
+ // }, [paramGroup, paramUser]);
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
+
+export default ExpensePage;
diff --git a/src/app/_components/expense/expense-card.tsx b/src/app/_components/expense/expense-card.tsx
index e2f7f9a..dc5a280 100644
--- a/src/app/_components/expense/expense-card.tsx
+++ b/src/app/_components/expense/expense-card.tsx
@@ -1,4 +1,3 @@
-import Avatar from "@/components/avatar";
import {
Card,
CardContent,
@@ -10,28 +9,81 @@ import {
Drawer,
DrawerClose,
DrawerContent,
- DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
-
import { Separator } from "@/components/ui/separator";
-import { getAmount } from "@/lib/utils";
-import type { Expense } from "@/server/db/schema";
+import { cn, getAmount } from "@/lib/utils";
+import type {
+ Expense,
+ ExpenseGroupBadge,
+ ExpenseSplit,
+} from "@/server/db/schema";
import React from "react";
import { UserBadge } from "./expense-participants";
import { Icons } from "@/components/icons";
-
import ExpenseDetails from "./expense-details";
import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { calculateTotalValue } from "@/lib/utils/expense";
-function ExpenseCard({ expense }: { expense: Expense }) {
+export const ExpenseSplitsList = ({
+ splits,
+}: {
+ splits: Array;
+}) => {
+ return (
+
+ {splits?.map((split, idx) => (
+
+
+ owes
+
+
+
+
+ {getAmount(Number(split.amount))}
+
+
+ ))}
+
+ );
+};
+
+export const ExpenseGroupBadgesList = ({
+ groupBadges,
+}: {
+ groupBadges: Array;
+}) => {
+ return (
+
+ {groupBadges.map(({ group }) => (
+
+
+
+ {group!.name}
+
+
+ ))}
+
+ );
+};
+
+function ExpenseCard({
+ expense,
+ sessionUserId,
+}: {
+ expense: Expense;
+ sessionUserId: string;
+}) {
+ const toatalBalance = calculateTotalValue(sessionUserId, expense.splits!);
+ const totalValue = toatalBalance.owed - toatalBalance.owes;
return (
-
+
@@ -43,36 +95,52 @@ function ExpenseCard({ expense }: { expense: Expense }) {
-
- {expense?.splits?.map((split, idx) => (
-
-
-
-
-
-
- {getAmount(Number(split.amount))}
-
-
- ))}
-
-
+ {expense.groupBadges?.length ? (
+
+ Groups:
+
+
+ ) : null}
-
- You Owe/Get
-
- {getAmount(Number(expense.amount))}
+
+
+
+
+
+ {expense?.splits!.length > 2 && (
+ <>
+
+
+ {expense?.splits!.length - 2} more
+
+ >
+ )}
+
+
+
+
+ You {totalValue < 0 ? "Owe" : "Get"}
+
+
+ {getAmount(Number(totalValue))}
- Are you absolutely sure?
- This action cannot be undone.
+
+ {expense.description}
+ {getAmount(Number(expense.amount))}
+
-
+
diff --git a/src/app/_components/expense/expense-details.tsx b/src/app/_components/expense/expense-details.tsx
index 02e0a71..1ce3604 100644
--- a/src/app/_components/expense/expense-details.tsx
+++ b/src/app/_components/expense/expense-details.tsx
@@ -1,8 +1,14 @@
import type { Expense } from "@/server/db/schema";
import React from "react";
+import { ExpenseGroupBadgesList, ExpenseSplitsList } from "./expense-card";
function ExpenseDetails({ expense }: { expense: Expense }) {
- return
ExpenseDetails for expense: {expense.id}
;
+ return (
+ <>
+
+
+ >
+ );
}
export default ExpenseDetails;
diff --git a/src/app/_components/expense/expense-form.tsx b/src/app/_components/expense/expense-form.tsx
index 1cc03a1..b95bdb1 100644
--- a/src/app/_components/expense/expense-form.tsx
+++ b/src/app/_components/expense/expense-form.tsx
@@ -17,12 +17,9 @@ import { expenseSchema } from "@/lib/validations/expense";
import { Textarea } from "@/components/ui/textarea";
import { NumberInput } from "@/components/number-input";
import { EuroIcon } from "lucide-react";
-import ExpenseSplit from "./expense-split";
import { toast } from "sonner";
import { api } from "@/trpc/react";
import { useExpenseStore } from "@/lib/store/expense-store";
-import ExpenseParticipants from "./expense-participants";
-import { Separator } from "@/components/ui/separator";
import { calculateDepts } from "@/lib/utils/expense";
import { cn } from "@/lib/utils";
import { useRouter } from "next/navigation";
@@ -53,10 +50,11 @@ function ExpenseForm({
const recalculateSplits = useExpenseStore((state) => state.recalculateSplits);
const amount = form.watch("amount");
const resetExpenseStore = useExpenseStore((state) => state.resetExpenseStore);
+ const groupBadges = useExpenseStore((state) => state.groupBadges);
const router = useRouter();
-
+
const createExpense = api.expense.create.useMutation({
- onSuccess(expense) {
+ onSuccess() {
form.reset();
resetExpenseStore();
addParticipants([sessionUser]);
@@ -73,6 +71,7 @@ function ExpenseForm({
});
},
});
+
function onSubmit(expense: z.infer
) {
if (createExpense.isPending) return;
if (participants.length <= 1)
@@ -89,14 +88,13 @@ function ExpenseForm({
payments
);
- createExpense.mutate({ expense, debs });
+ createExpense.mutate({
+ expense,
+ debs,
+ groupBadges: groupBadges.map(({ id }) => id),
+ });
}
- React.useEffect(() => {
- addParticipants([sessionUser]);
- setPayments([{ amount, userId: sessionUser.id }]);
- }, []);
-
const handleAmountChange = (value: number) => {
setAmount(value);
const firstUserId = payments[0]?.userId ?? sessionUser.id;
@@ -164,78 +162,6 @@ function ExpenseForm({
)}
/>
-
-
-
-
-
- {/*
-
- Expense
- Split
-
-
- (
-
- Amount
- {/*
-
-
-
-
-
-
- )}
- />
- (
-
- Description
-
-
-
-
-
- )}
- />
-
-
-
- {/* (
-
- Select Friend
-
-
-
-
-
- )}
- />
-
- */}
);
diff --git a/src/app/_components/expense/expense-participants.tsx b/src/app/_components/expense/expense-participants.tsx
index 49d4d3d..c58ad4c 100644
--- a/src/app/_components/expense/expense-participants.tsx
+++ b/src/app/_components/expense/expense-participants.tsx
@@ -1,10 +1,7 @@
"use client";
import Avatar from "@/components/avatar";
-import { Input } from "@/components/ui/input";
import { useExpenseStore } from "@/lib/store/expense-store";
import React from "react";
-import FriendSelect from "../friend/friend-select";
-import { api } from "@/trpc/react";
import { Button } from "@/components/ui/button";
import { XIcon } from "lucide-react";
import type { User } from "@/server/db/schema";
@@ -18,9 +15,9 @@ export const UserBadge = ({
children?: React.ReactNode;
}) => {
return (
-
+
-
{user.name}
+
{user.name}
{children}
diff --git a/src/app/_components/group/group-card.tsx b/src/app/_components/group/group-card.tsx
index c6eb4f1..50b4a77 100644
--- a/src/app/_components/group/group-card.tsx
+++ b/src/app/_components/group/group-card.tsx
@@ -1,5 +1,7 @@
+"use client";
import Avatar from "@/components/avatar";
import { Icons } from "@/components/icons";
+import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
@@ -7,11 +9,11 @@ import {
CardTitle,
} from "@/components/ui/card";
import type { Group } from "@/server/db/schema";
-import type { User } from "@clerk/nextjs/server";
-import Link from "next/link";
+import { useRouter } from "next/navigation";
import React from "react";
function GroupCard({ group }: { group: Group }) {
+ const router = useRouter();
return (
@@ -52,13 +54,16 @@ function GroupCard({ group }: { group: Group }) {
{group.description}
- {
+ e.preventDefault();
+ router.push(`/add?groupId=${group.id}`);
+ }}
>
Add
-
+
);
diff --git a/src/app/_components/group/group-page.tsx b/src/app/_components/group/group-page.tsx
index 47f1b7f..c4a4b04 100644
--- a/src/app/_components/group/group-page.tsx
+++ b/src/app/_components/group/group-page.tsx
@@ -11,10 +11,13 @@ import GroupFormDrawer from "./group-form-drawer";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Icons } from "@/components/icons";
+import ExpenseCard from "../expense/expense-card";
function GroupPage({ groupId }: { groupId: string }) {
const [group] = api.group.get.useSuspenseQuery({ id: groupId });
if (!group) return ;
+
+ const groupExpenses = group?.groupBadges?.map(({ expense }) => expense);
return (
<>
@@ -57,7 +60,11 @@ function GroupPage({ groupId }: { groupId: string }) {
+
+ {groupExpenses.map((expense) => (
+
+ ))}
{/* {group.members.map((member) => (
diff --git a/src/components/charts/radar-chart.tsx b/src/components/charts/radar-chart.tsx
new file mode 100644
index 0000000..7c9125c
--- /dev/null
+++ b/src/components/charts/radar-chart.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { TrendingUp } from "lucide-react";
+import {
+ PolarAngleAxis,
+ PolarGrid,
+ Radar,
+ RadarChart as RadarChartComponent,
+} from "recharts";
+
+import {
+ Card,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ type ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import { getAmount } from "@/lib/utils";
+import type { User } from "@/server/db/schema";
+import Avatar from "../avatar";
+
+type ChartData = {
+ user: User;
+ owesYou: number;
+ youOwe: number;
+ total: number;
+};
+
+const chartConfig = {
+ owesYou: {
+ label: "Owes You",
+ color: "var(--success)",
+ },
+ youOwe: {
+ label: "You Owe",
+ color: "var(--destrctive)",
+ },
+ total: {
+ label: "Total",
+ color: "var(--primary)",
+ },
+} satisfies ChartConfig;
+export function AnalyticsRadarChart({
+ chartData,
+}: {
+ chartData: Array;
+}) {
+ // Transform data to match radar chart expectations
+ const radarData = chartData.map((data) => ({
+ subject: data.user.id,
+ owesYou: data.owesYou,
+ youOwe: data.youOwe,
+ total: data.total,
+ fullMark: Math.max(data.owesYou, data.youOwe) * 1.2, // Add some headroom
+ }));
+
+ return (
+ <>
+
+
+
+ {
+ const user = chartData.find(
+ ({ user }) => user.id === payload.value
+ )!.user;
+
+ return (
+
+
+ {user.name}
+
+ );
+ }}
+ />
+ {/* */}
+
+
+ }
+ />
+
+
+ >
+ );
+}
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 64f010b..0158d1e 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -17,6 +17,8 @@ type IconName =
| "sun"
| "moon"
| "display"
+ | "arrowRight"
+ | "filter"
| "loading";
export const Icons: Record = {
@@ -398,6 +400,52 @@ export const Icons: Record = {
);
},
+ arrowRight(props) {
+ return (
+
+
+
+ );
+ },
+
+ filter(props) {
+ return (
+
+
+
+
+
+ );
+ },
+
loading(props) {
return (
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
new file mode 100644
index 0000000..0205413
--- /dev/null
+++ b/src/components/ui/badge.tsx
@@ -0,0 +1,46 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span"
+
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx
new file mode 100644
index 0000000..97cc280
--- /dev/null
+++ b/src/components/ui/chart.tsx
@@ -0,0 +1,353 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+function ChartContainer({
+ id,
+ className,
+ children,
+ config,
+ ...props
+}: React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+}) {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+