From 2ddbef6627f9878f3b266ac41eb6d13814f0f575 Mon Sep 17 00:00:00 2001 From: shrt Date: Mon, 7 Apr 2025 19:58:58 +0200 Subject: [PATCH] awesome ui changes --- package.json | 2 + pnpm-lock.yaml | 112 +++++++ src/app.config.ts | 12 +- src/app/(router)/add/page.tsx | 9 +- src/app/(router)/expense/page.tsx | 13 +- src/app/(router)/friend/page.tsx | 44 +-- src/app/(router)/group/page.tsx | 10 +- src/app/(router)/layout.tsx | 6 +- src/app/(router)/page.tsx | 9 +- src/app/_components/expense/expense-form.tsx | 128 ++++++-- src/app/_components/expense/expense-split.tsx | 148 +++++++++ .../expense/paid-by-custom-split.tsx | 86 ++++++ src/app/_components/expense/paid-by.tsx | 95 ++++++ src/app/_components/expense/split-expense.tsx | 7 - .../_components/friend/add-friend-drawer.tsx | 5 +- src/app/_components/friend/friend-card.tsx | 10 +- src/app/_components/friend/friend-list.tsx | 50 +++ src/app/_components/friend/friend-select.tsx | 26 +- .../friend/pending-request-button.tsx | 17 +- .../_components/group/create-group-drawer.tsx | 34 +++ src/app/_components/user-dropdown.tsx | 50 +++ src/app/layout.tsx | 2 +- src/components/combobox.tsx | 3 +- src/components/header.tsx | 9 +- src/components/icons.tsx | 286 ++++++++++++++++++ src/components/money-input.tsx | 190 ++++++++++++ src/components/nav-link.tsx | 47 ++- src/components/navbar.tsx | 4 +- src/components/number-input.tsx | 222 ++++---------- src/components/ui/avatar.tsx | 16 +- src/components/ui/button.tsx | 3 +- src/components/ui/card.tsx | 20 +- src/components/ui/command.tsx | 36 +-- src/components/ui/dialog.tsx | 47 +-- src/components/ui/dropdown-menu.tsx | 50 +-- src/components/ui/form.tsx | 81 ++--- src/components/ui/input.tsx | 1 - src/components/ui/label.tsx | 12 +- src/components/ui/popover.tsx | 18 +- src/components/ui/select.tsx | 185 +++++++++++ src/components/ui/separator.tsx | 28 ++ src/components/ui/skeleton.tsx | 6 +- src/components/ui/tabs.tsx | 18 +- src/hooks/use-expense-split.ts | 81 +++++ src/{index.d.ts => in.ts} | 1 + src/lib/utils/expense.ts | 117 +++++++ src/lib/{utils.ts => utils/index.ts} | 0 src/lib/validations/expense.ts | 3 - src/server/api/routers/expense.ts | 6 +- src/server/api/routers/friend.ts | 4 +- src/server/auth/config.ts | 1 + src/styles/globals.css | 79 ++--- 52 files changed, 1965 insertions(+), 484 deletions(-) create mode 100644 src/app/_components/expense/expense-split.tsx create mode 100644 src/app/_components/expense/paid-by-custom-split.tsx create mode 100644 src/app/_components/expense/paid-by.tsx delete mode 100644 src/app/_components/expense/split-expense.tsx create mode 100644 src/app/_components/friend/friend-list.tsx create mode 100644 src/app/_components/group/create-group-drawer.tsx create mode 100644 src/app/_components/user-dropdown.tsx create mode 100644 src/components/icons.tsx create mode 100644 src/components/money-input.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/hooks/use-expense-split.ts rename src/{index.d.ts => in.ts} (88%) create mode 100644 src/lib/utils/expense.ts rename src/lib/{utils.ts => utils/index.ts} (100%) diff --git a/package.json b/package.json index fce81ea..4c07b5e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", "@t3-oss/env-nextjs": "^0.12.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce78df9..fd07740 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,12 @@ importers: '@radix-ui/react-popover': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-select': + specifier: ^2.1.6 + version: 2.1.6(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-separator': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': specifier: ^1.1.2 version: 1.1.2(@types/react@19.1.0)(react@19.1.0) @@ -705,6 +711,9 @@ packages: '@petamoriken/float16@3.9.2': resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==} + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} @@ -948,6 +957,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.1.6': + resolution: {integrity: sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.2': + resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.1.2': resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} peerDependencies: @@ -1006,6 +1041,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.1.0': resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: @@ -1024,6 +1068,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-visually-hidden@1.1.2': + resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} @@ -2085,6 +2142,8 @@ snapshots: '@petamoriken/float16@3.9.2': {} + '@radix-ui/number@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} '@radix-ui/react-arrow@1.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': @@ -2334,6 +2393,44 @@ snapshots: '@types/react': 19.1.0 '@types/react-dom': 19.1.1(@types/react@19.1.0) + '@radix-ui/react-select@2.1.6(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-direction': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.1.2(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.0(@types/react@19.1.0)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + aria-hidden: 1.2.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.6.3(@types/react@19.1.0)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.1(@types/react@19.1.0) + + '@radix-ui/react-separator@1.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.1(@types/react@19.1.0) + '@radix-ui/react-slot@1.1.2(@types/react@19.1.0)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0) @@ -2383,6 +2480,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.0 + '@radix-ui/react-use-previous@1.1.0(@types/react@19.1.0)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.0 + '@radix-ui/react-use-rect@1.1.0(@types/react@19.1.0)(react@19.1.0)': dependencies: '@radix-ui/rect': 1.1.0 @@ -2397,6 +2500,15 @@ snapshots: optionalDependencies: '@types/react': 19.1.0 + '@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + '@types/react-dom': 19.1.1(@types/react@19.1.0) + '@radix-ui/rect@1.1.0': {} '@standard-schema/utils@0.3.0': {} diff --git a/src/app.config.ts b/src/app.config.ts index 74e4280..0ed1e52 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -9,27 +9,27 @@ export const appConfig: AppConfig = { { name: "Home", href: "/", - icon: "Home", + icon: "home", }, { name: "Expenses", href: "/expense", - icon: "Wallet", + icon: "wallet", }, { - name: "Add Expense", + name: "Add", href: "/add", - icon: "Plus", + icon: "addSquare", }, { name: "Groups", href: "/group", - icon: "Users", + icon: "group", }, { name: "Friends", href: "/friend", - icon: "User", + icon: "friends", }, ], }; diff --git a/src/app/(router)/add/page.tsx b/src/app/(router)/add/page.tsx index af36544..4a557c6 100644 --- a/src/app/(router)/add/page.tsx +++ b/src/app/(router)/add/page.tsx @@ -1,6 +1,7 @@ import ExpenseForm from "@/app/_components/expense/expense-form"; import Header from "@/components/header"; import Section from "@/components/section"; +import { Button } from "@/components/ui/button"; import { auth } from "@/server/auth"; import { api, HydrateClient } from "@/trpc/server"; import React from "react"; @@ -10,9 +11,13 @@ export default async function Page() { if (session?.user) void api.friend.getAll.prefetch(); return ( -
+
+ +
- +
); diff --git a/src/app/(router)/expense/page.tsx b/src/app/(router)/expense/page.tsx index 2394fdf..8445150 100644 --- a/src/app/(router)/expense/page.tsx +++ b/src/app/(router)/expense/page.tsx @@ -1,5 +1,16 @@ +import Header from "@/components/header"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; import React from "react"; export default function Page() { - return
Page
; + return ( + <> +
+ +
+ + ); } diff --git a/src/app/(router)/friend/page.tsx b/src/app/(router)/friend/page.tsx index 9a84e5a..37acb6c 100644 --- a/src/app/(router)/friend/page.tsx +++ b/src/app/(router)/friend/page.tsx @@ -1,48 +1,18 @@ import AddFriendDrawer from "@/app/_components/friend/add-friend-drawer"; -import FriendCard from "@/app/_components/friend/friend-card"; -import PendingRequestButton from "@/app/_components/friend/pending-request-button"; +import FriendList from "@/app/_components/friend/friend-list"; import Header from "@/components/header"; -import Section from "@/components/section"; -import { Button } from "@/components/ui/button"; -import { api } from "@/trpc/server"; -import Link from "next/link"; +import { api, HydrateClient } from "@/trpc/server"; import React from "react"; export default async function Page() { - const friendResult = await api.friend.getAll(); - const pendingFriends = friendResult.filter((f) => f.status === "pending"); - const friends = friendResult.filter((f) => f.status === "accepted"); + void api.friend.getAll.prefetch(); + return ( - <> +
- {pendingFriends.length ? ( -
- {pendingFriends.map(({ user, requestedBy, id }) => ( - - - - ))} -
- ) : null} - -
- {friends?.length ? ( - friends.map(({ user }) => ( - - - - )) - ) : ( -

No friends yet

- )} -
- + +
); } diff --git a/src/app/(router)/group/page.tsx b/src/app/(router)/group/page.tsx index 2394fdf..04330e3 100644 --- a/src/app/(router)/group/page.tsx +++ b/src/app/(router)/group/page.tsx @@ -1,5 +1,13 @@ +import CreateGroupDrawer from "@/app/_components/group/create-group-drawer"; +import Header from "@/components/header"; import React from "react"; export default function Page() { - return
Page
; + return ( + <> +
+ +
+ + ); } diff --git a/src/app/(router)/layout.tsx b/src/app/(router)/layout.tsx index 282b01f..cab1dec 100644 --- a/src/app/(router)/layout.tsx +++ b/src/app/(router)/layout.tsx @@ -11,8 +11,10 @@ export default async function Layout({ const session = await auth(); if (!session?.user) return redirect("/api/auth/signin"); return ( -
-
{children}
+
+
+ {children} +
); diff --git a/src/app/(router)/page.tsx b/src/app/(router)/page.tsx index ecccc87..f2f25bf 100644 --- a/src/app/(router)/page.tsx +++ b/src/app/(router)/page.tsx @@ -1,13 +1,18 @@ import { appConfig } from "@/app.config"; import Header from "@/components/header"; -import { ModeToggle } from "@/components/mode-toggle"; +import Section from "@/components/section"; +import { auth } from "@/server/auth"; +import UserDropdown from "../_components/user-dropdown"; export default async function Home() { + const session = await auth(); + return ( <>
- +
+
); } diff --git a/src/app/_components/expense/expense-form.tsx b/src/app/_components/expense/expense-form.tsx index 7797d82..b2ec971 100644 --- a/src/app/_components/expense/expense-form.tsx +++ b/src/app/_components/expense/expense-form.tsx @@ -15,33 +15,92 @@ import { } from "@/components/ui/form"; import { expenseSchema } from "@/lib/validations/expense"; import { Textarea } from "@/components/ui/textarea"; -import { MoneyInput } from "@/components/number-input"; -import FriendSelect from "../friend/friend-select"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -function ExpenseForm({ initialExpense }: { initialExpense?: Expense }) { +import { NumberInput } from "@/components/number-input"; +import { EuroIcon } from "lucide-react"; +import ExpenseSplit from "./expense-split"; +import type { Session } from "next-auth"; +import { useExpenseSplit } from "@/hooks/use-expense-split"; +import { toast } from "sonner"; +import { api } from "@/trpc/react"; + +function ExpenseForm({ + initialExpense, + session, + hideSubmit, +}: { + initialExpense?: Expense; + session: Session; + hideSubmit?: boolean; +}) { + const createExpense = api.expense.create.useMutation(); const form = useForm>({ resolver: zodResolver(expenseSchema), defaultValues: { description: initialExpense?.description ?? "", amount: initialExpense?.amount ?? 0, - groupId: initialExpense?.groupId ?? "", }, }); + const amount = form.watch("amount"); + const expenseSplitHook = useExpenseSplit(amount, session); function onSubmit(values: z.infer) { - console.log(values); + if (expenseSplitHook.participants.length <= 1) + return toast.error("Please add at least 2 participants"); + if (expenseSplitHook.splitType !== "Equal") + return toast.error("You can only split equal for now."); + toast.success("Expense created!"); } - return (
- -
+ + {!hideSubmit && ( -
- + )} + ( + + Amount + +
+ + +
+ +
+ )} + /> + ( + + Description + +