added groups
This commit is contained in:
parent
a2492fefbd
commit
3fc08a3c10
22
src/app/(router)/group/[id]/page.tsx
Normal file
22
src/app/(router)/group/[id]/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import GroupPage from "@/app/_components/group/group-page";
|
||||
import { api, HydrateClient } from "@/trpc/server";
|
||||
|
||||
async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
void api.group.get.prefetch({ id });
|
||||
|
||||
return (
|
||||
<HydrateClient>
|
||||
<GroupPage groupId={id} />
|
||||
</HydrateClient>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@ -1,13 +1,29 @@
|
||||
import CreateGroupDrawer from "@/app/_components/group/create-group-drawer";
|
||||
import Header from "@/components/header";
|
||||
import React from "react";
|
||||
import GroupFormDrawer from "@/app/_components/group/group-form-drawer";
|
||||
import Header from "@/components/header";
|
||||
import Section from "@/components/section";
|
||||
import { api } from "@/trpc/server";
|
||||
import Link from "next/link";
|
||||
import GroupCard from "@/app/_components/group/group-card";
|
||||
|
||||
export default function Page() {
|
||||
export default async function Page() {
|
||||
const groups = await api.group.getAll();
|
||||
return (
|
||||
<>
|
||||
<Header text="Groups">
|
||||
<CreateGroupDrawer />
|
||||
<GroupFormDrawer />
|
||||
</Header>
|
||||
<Section>
|
||||
<menu className="space-y-4">
|
||||
{groups.map((group) => (
|
||||
<li key={group.id}>
|
||||
<Link href={`/group/${group.id}`}>
|
||||
<GroupCard group={group} />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</menu>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -31,10 +31,12 @@ function ExpenseCard({ expense }: { expense: Expense }) {
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger className="w-full">
|
||||
<Card className="bg-card/25 border-0 shadow-none gap-2 pb-0">
|
||||
<Card className=" border-0 shadow-none gap-2 pb-0">
|
||||
<CardHeader className="px-4">
|
||||
<CardTitle className="flex items-center w-full">
|
||||
<p className="w-full max-w-64 truncate ">{expense.description}</p>
|
||||
<p className="w-full max-w-64 truncate text-start ">
|
||||
{expense.description}
|
||||
</p>
|
||||
<span className="ml-auto">
|
||||
{getAmount(Number(expense.amount))}
|
||||
</span>
|
||||
|
||||
@ -15,8 +15,8 @@ import { Input } from "@/components/ui/input";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { toast } from "sonner";
|
||||
import type { PublicUser } from "next-auth";
|
||||
import FriendCard from "./friend-card";
|
||||
import UserCard from "../user-card";
|
||||
import type { User } from "@/server/db/schema";
|
||||
|
||||
const SearchResultCard = ({
|
||||
addFriend,
|
||||
@ -25,10 +25,10 @@ const SearchResultCard = ({
|
||||
}: {
|
||||
addFriend: (userId: string) => void;
|
||||
pending: boolean;
|
||||
user: PublicUser;
|
||||
user: User;
|
||||
}) => {
|
||||
return (
|
||||
<FriendCard user={user}>
|
||||
<UserCard user={user}>
|
||||
<Button
|
||||
size={"sm"}
|
||||
className="ml-auto"
|
||||
@ -40,7 +40,7 @@ const SearchResultCard = ({
|
||||
>
|
||||
{pending ? "Pending" : "Add Friend"}
|
||||
</Button>
|
||||
</FriendCard>
|
||||
</UserCard>
|
||||
);
|
||||
};
|
||||
|
||||
@ -50,11 +50,7 @@ export default function AddFriendDrawer() {
|
||||
const [value, setValue] = React.useState("");
|
||||
const [searchValue] = useDebounce(value, 1000);
|
||||
|
||||
const {
|
||||
data: searchResult,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = api.friend.search.useQuery(
|
||||
const { data: searchResult, isFetching } = api.friend.search.useQuery(
|
||||
{
|
||||
search: searchValue,
|
||||
},
|
||||
@ -70,6 +66,7 @@ export default function AddFriendDrawer() {
|
||||
utils.friend.getAll.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const handleAddFriend = (userId: string) => {
|
||||
addFriend.mutate({
|
||||
userId,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import FriendCard from "@/app/_components/friend/friend-card";
|
||||
import UserCard from "@/app/_components/user-card";
|
||||
import PendingRequestButton from "@/app/_components/friend/pending-request-button";
|
||||
import Section from "@/components/section";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -16,19 +16,19 @@ function FriendList() {
|
||||
{pendingFriends.length ? (
|
||||
<Section heading="Requests">
|
||||
{pendingFriends.map(({ user, requestedBy, id }) => (
|
||||
<FriendCard key={user.id} user={user}>
|
||||
<UserCard key={user.id} user={user}>
|
||||
<PendingRequestButton
|
||||
friendshipId={id}
|
||||
requestedBy={requestedBy}
|
||||
/>
|
||||
</FriendCard>
|
||||
</UserCard>
|
||||
))}
|
||||
</Section>
|
||||
) : null}
|
||||
<Section heading={`Friends (${friends?.length ?? 0})`}>
|
||||
{friends?.length ? (
|
||||
friends.map(({ user }) => (
|
||||
<FriendCard key={user.id} user={user}>
|
||||
<UserCard key={user.id} user={user}>
|
||||
<Button
|
||||
size={"sm"}
|
||||
className="ml-auto"
|
||||
@ -37,7 +37,7 @@ function FriendList() {
|
||||
>
|
||||
<Link href={`/add?userId=${user.id}`}>Add Expense</Link>
|
||||
</Button>
|
||||
</FriendCard>
|
||||
</UserCard>
|
||||
))
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">No friends yet</p>
|
||||
|
||||
126
src/app/_components/group/add-to-group-drawer.tsx
Normal file
126
src/app/_components/group/add-to-group-drawer.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/trpc/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import type { User } from "@/server/db/schema";
|
||||
import UserCard from "../user-card";
|
||||
|
||||
const SearchResultCard = ({
|
||||
addUser,
|
||||
user,
|
||||
}: {
|
||||
addUser: (userId: string) => void;
|
||||
|
||||
user: User;
|
||||
}) => {
|
||||
return (
|
||||
<UserCard user={user}>
|
||||
<Button
|
||||
size={"sm"}
|
||||
className="ml-auto"
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
addUser(user.id!);
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</UserCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default function AddToGroupDrawer({
|
||||
groupId,
|
||||
className,
|
||||
}: {
|
||||
groupId: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [value, setValue] = React.useState("");
|
||||
const [searchValue] = useDebounce(value, 1000);
|
||||
|
||||
const { data: searchResult, isFetching } = api.user.search.useQuery(
|
||||
{
|
||||
search: searchValue,
|
||||
},
|
||||
{
|
||||
enabled: !!searchValue,
|
||||
}
|
||||
);
|
||||
const addFriend = api.group.addMember.useMutation({
|
||||
onSuccess() {
|
||||
toast.success("User added to group.");
|
||||
setOpen(false);
|
||||
setValue("");
|
||||
utils.group.get.invalidate();
|
||||
utils.group.getAll.invalidate();
|
||||
},
|
||||
});
|
||||
const handleAddFriend = (userId: string) => {
|
||||
addFriend.mutate({
|
||||
groupId,
|
||||
userId,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button size={"sm"} className={className}>
|
||||
Add People
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Add a user to group</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="px-4 space-y-4">
|
||||
<Input
|
||||
autoFocus
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
placeholder="Search for a user"
|
||||
/>
|
||||
<ul className="space-y-2">
|
||||
{isFetching ? (
|
||||
<Skeleton className="h-6 w-full" />
|
||||
) : searchResult?.length ? (
|
||||
searchResult.map((user) => (
|
||||
<li key={user.id}>
|
||||
<SearchResultCard addUser={handleAddFriend} user={user} />
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{searchValue.length
|
||||
? "No results found"
|
||||
: "Start typing to search for a user"}
|
||||
</p>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
15
src/app/_components/group/group-card.tsx
Normal file
15
src/app/_components/group/group-card.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { Group } from "@/server/db/schema";
|
||||
import React from "react";
|
||||
|
||||
function GroupCard({ group }: { group: Group }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{group.name}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupCard;
|
||||
@ -10,24 +10,28 @@ import {
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import GroupForm from "./group-form";
|
||||
import type { Group } from "@/server/db/schema";
|
||||
|
||||
export default function CreateGroupDrawer() {
|
||||
export default function GroupFormDrawer({
|
||||
initialGroup,
|
||||
}: {
|
||||
initialGroup?: Group;
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const existing = Boolean(initialGroup?.id?.length);
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button size={"sm"}>Create Group</Button>
|
||||
<Button size={"sm"}>
|
||||
{existing ? "Update" : "Create a new"} Group
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Create Group</DrawerTitle>
|
||||
<DrawerTitle>{existing ? "Update" : "Create"} Group</DrawerTitle>
|
||||
<GroupForm initialGroup={initialGroup} />
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
100
src/app/_components/group/group-form.tsx
Normal file
100
src/app/_components/group/group-form.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(2, {
|
||||
message: "Username must be at least 2 characters.",
|
||||
}),
|
||||
});
|
||||
|
||||
import React from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { groupSchema } from "@/lib/validations/group";
|
||||
import { api } from "@/trpc/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { Group } from "@/server/db/schema";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
function GroupForm({ initialGroup }: { initialGroup?: Group }) {
|
||||
const router = useRouter();
|
||||
const utils = api.useUtils();
|
||||
const createGroup = api.group.create.useMutation({
|
||||
onSuccess(group) {
|
||||
router.push(`/group/${group?.id}`);
|
||||
utils.invalidate();
|
||||
},
|
||||
});
|
||||
const updateGroup = api.group.update.useMutation({
|
||||
onSuccess() {
|
||||
utils.group.get.invalidate({ id: initialGroup?.id });
|
||||
},
|
||||
});
|
||||
const form = useForm<z.infer<typeof groupSchema>>({
|
||||
resolver: zodResolver(groupSchema),
|
||||
defaultValues: {
|
||||
name: initialGroup?.name ?? "",
|
||||
description: initialGroup?.description ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
const existing = Boolean(initialGroup?.id?.length);
|
||||
|
||||
function onSubmit(group: z.infer<typeof groupSchema>) {
|
||||
if (existing) {
|
||||
updateGroup.mutate({ group, groupId: initialGroup?.id! });
|
||||
} else {
|
||||
createGroup.mutate({ group });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoFocus={!existing}
|
||||
placeholder="What is your groups name?"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea placeholder="What is your groups name?" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full">
|
||||
{existing ? "Update" : "Create"} Group
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupForm;
|
||||
76
src/app/_components/group/group-page.tsx
Normal file
76
src/app/_components/group/group-page.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { api } from "@/trpc/react";
|
||||
import AddToGroupDrawer from "@/app/_components/group/add-to-group-drawer";
|
||||
import RemoveFromGroupButton from "@/app/_components/group/remove-from-group-button";
|
||||
import UserCard from "@/app/_components/user-card";
|
||||
import Header from "@/components/header";
|
||||
import Section from "@/components/section";
|
||||
import GroupFormDrawer from "./group-form-drawer";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Icons } from "@/components/icons";
|
||||
|
||||
function GroupPage({ groupId }: { groupId: string }) {
|
||||
const [group] = api.group.get.useSuspenseQuery({ id: groupId });
|
||||
if (!group) return <GroupFormDrawer />;
|
||||
return (
|
||||
<>
|
||||
<Header text={group.name}>
|
||||
<GroupFormDrawer initialGroup={group} />
|
||||
</Header>
|
||||
<Section>
|
||||
<p className="text-muted-foreground text-sm">{group.description}</p>
|
||||
<div className="flex gap-4 w-full items-center">
|
||||
<h4 className="text-lg font-semibold">
|
||||
Members ({group.members.length})
|
||||
</h4>
|
||||
<AddToGroupDrawer groupId={group.id} className="ml-auto" />
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{group.members.map((member) => (
|
||||
<li key={member.id}>
|
||||
<UserCard className="border bg-background" user={member.user!}>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{member.role}
|
||||
</span>
|
||||
<RemoveFromGroupButton
|
||||
userId={member.user.id}
|
||||
groupId={group.id}
|
||||
/>
|
||||
</UserCard>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex gap-4 w-full items-center">
|
||||
<h4 className="text-lg font-semibold">Expenses</h4>
|
||||
<Button asChild size={"sm"} className="ml-auto">
|
||||
<Link href={`/add?groupId=${group.id}`}>
|
||||
<Icons.wallet />
|
||||
<span>Add Expense</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{/* {group.members.map((member) => (
|
||||
<li key={member.id}>
|
||||
<UserCard className="border bg-background" user={member.user!}>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{member.role}
|
||||
</span>
|
||||
<RemoveFromGroupButton
|
||||
userId={member.user.id}
|
||||
groupId={group.id}
|
||||
/>
|
||||
</UserCard>
|
||||
</li>
|
||||
))} */}
|
||||
</ul>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupPage;
|
||||
45
src/app/_components/group/remove-from-group-button.tsx
Normal file
45
src/app/_components/group/remove-from-group-button.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { Icons } from "@/components/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/trpc/react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
function RemoveFromGroupButton({
|
||||
groupId,
|
||||
userId,
|
||||
}: {
|
||||
groupId: string;
|
||||
userId: string;
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const removeMember = api.group.removeMember.useMutation({
|
||||
onSuccess() {
|
||||
toast.success("User removed from group.");
|
||||
utils.invalidate();
|
||||
},
|
||||
});
|
||||
const handleClick = () => {
|
||||
removeMember.mutate({
|
||||
groupId,
|
||||
userId,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
className="ml-auto text-sm p-1 px-2 size-max"
|
||||
onClick={handleClick}
|
||||
disabled={removeMember.isPending}
|
||||
>
|
||||
{!removeMember.isPending ? (
|
||||
<Icons.trash className="size-4" />
|
||||
) : (
|
||||
<Icons.loading className="size-4" />
|
||||
)}
|
||||
<span>Remove</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveFromGroupButton;
|
||||
@ -1,21 +1,21 @@
|
||||
import React from "react";
|
||||
import Avatar from "@/components/avatar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { PublicUser } from "next-auth";
|
||||
import React from "react";
|
||||
import type { User } from "@/server/db/schema";
|
||||
|
||||
export default function FriendCard({
|
||||
export default function UserCard({
|
||||
user: { name, image },
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
user: PublicUser;
|
||||
user: User;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-2 hover:bg-accent/25 rounded-md",
|
||||
"flex items-center gap-2 p-2 hover:bg-accent/25 rounded-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@ -13,7 +13,8 @@ type IconName =
|
||||
| "trash"
|
||||
| "user"
|
||||
| "addSquare"
|
||||
| "check";
|
||||
| "check"
|
||||
| "loading";
|
||||
|
||||
export const Icons: Record<IconName, IconComponent> = {
|
||||
logo(props) {
|
||||
@ -301,4 +302,64 @@ export const Icons: Record<IconName, IconComponent> = {
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
loading(props) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
|
||||
<circle
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="15"
|
||||
r="15"
|
||||
cx="40"
|
||||
cy="100"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
calcMode="spline"
|
||||
dur="2"
|
||||
values="1;0;1;"
|
||||
keySplines=".5 0 .5 1;.5 0 .5 1"
|
||||
repeatCount="indefinite"
|
||||
begin="-.4"
|
||||
></animate>
|
||||
</circle>
|
||||
<circle
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="15"
|
||||
r="15"
|
||||
cx="100"
|
||||
cy="100"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
calcMode="spline"
|
||||
dur="2"
|
||||
values="1;0;1;"
|
||||
keySplines=".5 0 .5 1;.5 0 .5 1"
|
||||
repeatCount="indefinite"
|
||||
begin="-.2"
|
||||
></animate>
|
||||
</circle>
|
||||
<circle
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="15"
|
||||
r="15"
|
||||
cx="160"
|
||||
cy="100"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
calcMode="spline"
|
||||
dur="2"
|
||||
values="1;0;1;"
|
||||
keySplines=".5 0 .5 1;.5 0 .5 1"
|
||||
repeatCount="indefinite"
|
||||
begin="0"
|
||||
></animate>
|
||||
</circle>
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
"bg-card/25 text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
2
src/index.d.ts
vendored
2
src/index.d.ts
vendored
@ -5,3 +5,5 @@ type NavLink = {
|
||||
icon?: string;
|
||||
big?: boolean;
|
||||
};
|
||||
|
||||
type GroupRoles = "admin" | "member";
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const groupSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
name: z
|
||||
.string()
|
||||
.min(2, { message: "Group name must be at least 2 characters." }),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const groupMemberSchema = z.object({
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||
import { expenseRouter, friendRouter, userRouter } from "./routers";
|
||||
import {
|
||||
expenseRouter,
|
||||
friendRouter,
|
||||
groupRouter,
|
||||
userRouter,
|
||||
} from "./routers";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@ -10,6 +15,7 @@ export const appRouter = createTRPCRouter({
|
||||
expense: expenseRouter,
|
||||
friend: friendRouter,
|
||||
user: userRouter,
|
||||
group: groupRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
89
src/server/api/routers/group.ts
Normal file
89
src/server/api/routers/group.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { groupSchema } from "@/lib/validations/group";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { groupMembers, groups } from "@/server/db/schema";
|
||||
import { z } from "zod";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export const groupRouter = createTRPCRouter({
|
||||
get: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const group = await ctx.db.query.groups.findFirst({
|
||||
where: eq(groups.id, input.id),
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (group?.members?.find((member) => member.userId === ctx.auth.userId))
|
||||
return group;
|
||||
return undefined;
|
||||
}),
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
const membersShips = await ctx.db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, ctx.auth.userId),
|
||||
with: {
|
||||
group: true,
|
||||
},
|
||||
});
|
||||
return membersShips?.map(({ group }) => group!) ?? [];
|
||||
}),
|
||||
|
||||
// mutations
|
||||
create: protectedProcedure
|
||||
.input(z.object({ group: groupSchema }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const [group] = await ctx.db
|
||||
.insert(groups)
|
||||
.values({
|
||||
...input.group,
|
||||
createdById: ctx.auth.userId,
|
||||
})
|
||||
.returning({ id: groups.id });
|
||||
if (!group?.id?.length) throw new Error("Group cant get created");
|
||||
await ctx.db.insert(groupMembers).values({
|
||||
groupId: group.id,
|
||||
userId: ctx.auth.userId,
|
||||
});
|
||||
return group;
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(z.object({ group: groupSchema, groupId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
console.log("Check if groupmember is admin!!");
|
||||
return await ctx.db
|
||||
.update(groups)
|
||||
.set(input.group)
|
||||
.where(eq(groups.id, input.groupId))
|
||||
.returning({ id: groups.id });
|
||||
}),
|
||||
|
||||
addMember: protectedProcedure
|
||||
.input(z.object({ groupId: z.string(), userId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db.insert(groupMembers).values({
|
||||
groupId: input.groupId,
|
||||
userId: input.userId,
|
||||
});
|
||||
revalidatePath(`/group/${input.groupId}`);
|
||||
}),
|
||||
|
||||
removeMember: protectedProcedure
|
||||
.input(z.object({ groupId: z.string(), userId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db
|
||||
.delete(groupMembers)
|
||||
.where(
|
||||
and(
|
||||
eq(groupMembers.groupId, input.groupId),
|
||||
eq(groupMembers.userId, input.userId)
|
||||
)
|
||||
);
|
||||
revalidatePath(`/group/${input.groupId}`);
|
||||
}),
|
||||
});
|
||||
@ -1,3 +1,4 @@
|
||||
export { friendRouter } from "./friend";
|
||||
export { expenseRouter } from "./expense";
|
||||
export { userRouter } from "./user";
|
||||
export { groupRouter } from "./group";
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq, ilike, inArray, not } from "drizzle-orm";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { users } from "@/server/db/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
getSessionUser: protectedProcedure.query(async ({ ctx }) => {
|
||||
@ -9,4 +10,27 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
return user;
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
search: z.string(),
|
||||
excludedIds: z.array(z.string()).optional(),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userId = ctx.auth.userId;
|
||||
const searchResult = await ctx.db.query.users.findMany({
|
||||
where: and(
|
||||
not(eq(users.id, userId)),
|
||||
ilike(users.name, `%${input.search}%`),
|
||||
input.excludedIds?.length
|
||||
? not(inArray(users.id, input.excludedIds))
|
||||
: undefined
|
||||
),
|
||||
});
|
||||
console.log("search results", searchResult);
|
||||
|
||||
return searchResult;
|
||||
}),
|
||||
});
|
||||
|
||||
@ -151,6 +151,7 @@ export const groups = createTable(
|
||||
(d) => ({
|
||||
id: d.varchar().primaryKey().$defaultFn(createId),
|
||||
name: d.varchar({ length: 255 }).notNull(),
|
||||
description: d.text(),
|
||||
createdById: d
|
||||
.varchar()
|
||||
.notNull()
|
||||
@ -189,6 +190,11 @@ export const groupMembers = createTable(
|
||||
.varchar()
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
role: d
|
||||
.varchar({ length: 10 })
|
||||
.notNull()
|
||||
.$type<GroupRoles>()
|
||||
.default("admin"),
|
||||
joinedAt: d
|
||||
.timestamp({ withTimezone: true })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user