migrated from auth.js to clerk; some ui remake; added expense-card
This commit is contained in:
parent
cd8a8d746d
commit
a2492fefbd
@ -21,8 +21,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/drizzle-adapter": "^1.7.2",
|
"@auth/drizzle-adapter": "^1.7.2",
|
||||||
"@capacitor/cli": "^7.2.0",
|
"@clerk/nextjs": "^6.14.3",
|
||||||
"@capacitor/core": "^7.2.0",
|
"@clerk/themes": "^2.2.31",
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.3",
|
"@radix-ui/react-avatar": "^1.1.3",
|
||||||
@ -46,7 +46,6 @@
|
|||||||
"drizzle-orm": "^0.41.0",
|
"drizzle-orm": "^0.41.0",
|
||||||
"lucide-react": "^0.487.0",
|
"lucide-react": "^0.487.0",
|
||||||
"next": "^15.2.3",
|
"next": "^15.2.3",
|
||||||
"next-auth": "5.0.0-beta.25",
|
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
@ -56,6 +55,7 @@
|
|||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
|
"svix": "^1.64.0",
|
||||||
"tailwind-merge": "^3.1.0",
|
"tailwind-merge": "^3.1.0",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
|
|||||||
1088
pnpm-lock.yaml
generated
1088
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,22 +1,24 @@
|
|||||||
import ExpenseForm from "@/app/_components/expense/expense-form";
|
import ExpenseForm from "@/app/_components/expense/expense-form";
|
||||||
import Header from "@/components/header";
|
import Header from "@/components/header";
|
||||||
import Section from "@/components/section";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { auth } from "@/server/auth";
|
|
||||||
import { api, HydrateClient } from "@/trpc/server";
|
import { api, HydrateClient } from "@/trpc/server";
|
||||||
|
import { currentUser } from "@clerk/nextjs/server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const session = await auth();
|
const user = await currentUser();
|
||||||
if (session?.user) void api.friend.getAll.prefetch();
|
if (user) void api.friend.getAll.prefetch();
|
||||||
|
const sessionUser = await api.user.getSessionUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HydrateClient>
|
<HydrateClient>
|
||||||
<Header text="Add Expense">
|
<Header text="Add Expense">
|
||||||
<Button type="submit" form="expense-form">
|
<Button type="submit" form="expense-form" size={"sm"}>
|
||||||
Create Expense
|
Create Expense
|
||||||
</Button>
|
</Button>
|
||||||
</Header>
|
</Header>
|
||||||
<ExpenseForm hideSubmit session={session!} />
|
<ExpenseForm hideSubmit sessionUser={sessionUser!} />
|
||||||
</HydrateClient>
|
</HydrateClient>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import ExpenseCard from "@/app/_components/expense/expense-card";
|
||||||
import Header from "@/components/header";
|
import Header from "@/components/header";
|
||||||
import Section from "@/components/section";
|
import Section from "@/components/section";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -7,20 +8,26 @@ import React from "react";
|
|||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const splits = await api.expense.getAll();
|
const splits = await api.expense.getAll();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header text="Expenses">
|
<Header text="Expenses">
|
||||||
<Button asChild>
|
<Button asChild size={"sm"}>
|
||||||
<Link href="/add">Add Expense</Link>
|
<Link href="/add">Add Expense</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</Header>
|
</Header>
|
||||||
<Section>
|
<Section>
|
||||||
{splits.map(({ expense }) => (
|
<div className="p-4 rounded-md flex items-center justify-center text-muted-foreground bg-card/25">
|
||||||
<div key={expense.id}>
|
Expense Stats
|
||||||
<h2>{expense.description}</h2>
|
|
||||||
<p>{expense.amount}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{splits.map((expense) => (
|
||||||
|
<li key={expense.id}>
|
||||||
|
<ExpenseCard expense={expense} />
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
|
</ul>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import AddFriendDrawer from "@/app/_components/friend/add-friend-drawer";
|
import AddFriendDrawer from "@/app/_components/friend/add-friend-drawer";
|
||||||
import FriendList from "@/app/_components/friend/friend-list";
|
import FriendList from "@/app/_components/friend/friend-list";
|
||||||
import Header from "@/components/header";
|
import Header from "@/components/header";
|
||||||
import { auth } from "@/server/auth";
|
|
||||||
import { api, HydrateClient } from "@/trpc/server";
|
import { api, HydrateClient } from "@/trpc/server";
|
||||||
|
import { currentUser } from "@clerk/nextjs/server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const session = await auth();
|
const user = await currentUser();
|
||||||
if (session?.user) void api.friend.getAll.prefetch();
|
if (user) void api.friend.getAll.prefetch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HydrateClient>
|
<HydrateClient>
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import Navbar from "@/components/navbar";
|
import Navbar from "@/components/navbar";
|
||||||
import { auth } from "@/server/auth";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default async function Layout({
|
export default async function Layout({
|
||||||
@ -8,11 +6,9 @@ export default async function Layout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const session = await auth();
|
|
||||||
if (!session?.user) return redirect("/api/auth/signin");
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 w-full mx-auto max-w h-screen flex flex-col justify-between bg-card/25 overflow-hidden">
|
<div className="space-y-2 w-full mx-auto max-w h-screen flex flex-col justify-between bg-card/25 overflow-hidden">
|
||||||
<main className="space-y-4 rounded-b-[3rem] bg-background h-full border border-card z-20 relative overflow-y-auto pb-8">
|
<main className="space-y-4 rounded-b-[3rem] bg-background/75 h-full border border-card z-20 relative overflow-y-auto pb-8">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import { appConfig } from "@/app.config";
|
import { appConfig } from "@/app.config";
|
||||||
import Header from "@/components/header";
|
import Header from "@/components/header";
|
||||||
import Section from "@/components/section";
|
import Section from "@/components/section";
|
||||||
import { auth } from "@/server/auth";
|
|
||||||
import UserDropdown from "../_components/user-dropdown";
|
import UserDropdown from "../_components/user-dropdown";
|
||||||
import { ModeToggle } from "@/components/mode-toggle";
|
import { ModeToggle } from "@/components/mode-toggle";
|
||||||
|
import { UserButton } from "@clerk/nextjs";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const session = await auth();
|
// const session = await auth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header text={appConfig.name}>
|
<Header text={appConfig.name}>
|
||||||
<UserDropdown user={session?.user!} />
|
<UserButton />
|
||||||
|
{/* <UserDropdown user={session?.user!} /> */}
|
||||||
</Header>
|
</Header>
|
||||||
<Section>
|
<Section>
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
|
|||||||
7
src/app/(router)/sign-in/page.tsx
Normal file
7
src/app/(router)/sign-in/page.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function Page() {
|
||||||
|
return <div>SignIn page</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page;
|
||||||
87
src/app/_components/expense/expense-card.tsx
Normal file
87
src/app/_components/expense/expense-card.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import Avatar from "@/components/avatar";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
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 React from "react";
|
||||||
|
import { UserBadge } from "./expense-participants";
|
||||||
|
import { Icons } from "@/components/icons";
|
||||||
|
|
||||||
|
import ExpenseDetails from "./expense-details";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
function ExpenseCard({ expense }: { expense: Expense }) {
|
||||||
|
return (
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger className="w-full">
|
||||||
|
<Card className="bg-card/25 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>
|
||||||
|
<span className="ml-auto">
|
||||||
|
{getAmount(Number(expense.amount))}
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="px-4">
|
||||||
|
{expense?.splits?.map((split, idx) => (
|
||||||
|
<div key={idx} className="text-sm flex items-center gap-1">
|
||||||
|
<UserBadge user={split.owedTo!} />
|
||||||
|
<Icons.brokenWallet className="size-4 " />
|
||||||
|
<UserBadge user={split.owedFrom!} />
|
||||||
|
|
||||||
|
<span className="font-semibold ml-auto ">
|
||||||
|
{getAmount(Number(split.amount))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Separator className="my-2" />
|
||||||
|
|
||||||
|
<CardFooter className="px-4 pb-4 flex items-center">
|
||||||
|
<span className="text-sm">You Owe/Get</span>
|
||||||
|
<span className="ml-auto font-semibold">
|
||||||
|
{getAmount(Number(expense.amount))}
|
||||||
|
</span>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent>
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>Are you absolutely sure?</DrawerTitle>
|
||||||
|
<DrawerDescription>This action cannot be undone.</DrawerDescription>
|
||||||
|
</DrawerHeader>
|
||||||
|
<div className="p-4">
|
||||||
|
<ExpenseDetails expense={expense} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DrawerFooter>
|
||||||
|
<DrawerClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerFooter>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpenseCard;
|
||||||
8
src/app/_components/expense/expense-details.tsx
Normal file
8
src/app/_components/expense/expense-details.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import type { Expense } from "@/server/db/schema";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function ExpenseDetails({ expense }: { expense: Expense }) {
|
||||||
|
return <div>ExpenseDetails for expense: {expense.id}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpenseDetails;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Expense } from "@/server/db/schema";
|
import type { Expense, User } from "@/server/db/schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@ -19,7 +19,6 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { NumberInput } from "@/components/number-input";
|
import { NumberInput } from "@/components/number-input";
|
||||||
import { EuroIcon } from "lucide-react";
|
import { EuroIcon } from "lucide-react";
|
||||||
import ExpenseSplit from "./expense-split";
|
import ExpenseSplit from "./expense-split";
|
||||||
import type { Session } from "next-auth";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { api } from "@/trpc/react";
|
import { api } from "@/trpc/react";
|
||||||
import { useExpenseStore } from "@/lib/store/expense-store";
|
import { useExpenseStore } from "@/lib/store/expense-store";
|
||||||
@ -30,13 +29,13 @@ import { cn } from "@/lib/utils";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
function ExpenseForm({
|
function ExpenseForm({
|
||||||
session,
|
|
||||||
initialExpense,
|
initialExpense,
|
||||||
hideSubmit,
|
hideSubmit,
|
||||||
|
sessionUser,
|
||||||
}: {
|
}: {
|
||||||
session: Session;
|
|
||||||
initialExpense?: Expense;
|
initialExpense?: Expense;
|
||||||
hideSubmit?: boolean;
|
hideSubmit?: boolean;
|
||||||
|
sessionUser: User;
|
||||||
}) {
|
}) {
|
||||||
const form = useForm<z.infer<typeof expenseSchema>>({
|
const form = useForm<z.infer<typeof expenseSchema>>({
|
||||||
resolver: zodResolver(expenseSchema),
|
resolver: zodResolver(expenseSchema),
|
||||||
@ -61,8 +60,8 @@ function ExpenseForm({
|
|||||||
onSuccess(expense) {
|
onSuccess(expense) {
|
||||||
form.reset();
|
form.reset();
|
||||||
resetExpenseStore();
|
resetExpenseStore();
|
||||||
addParticipant(session.user);
|
addParticipant(sessionUser);
|
||||||
setPayments([{ amount: amount, userId: session.user.id }]);
|
setPayments([{ amount: amount, userId: sessionUser.id }]);
|
||||||
toast.message("Expense Created", {
|
toast.message("Expense Created", {
|
||||||
position: "bottom-center",
|
position: "bottom-center",
|
||||||
action: {
|
action: {
|
||||||
@ -95,13 +94,13 @@ function ExpenseForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
addParticipant(session.user);
|
addParticipant(sessionUser);
|
||||||
setPayments([{ amount, userId: session.user.id }]);
|
setPayments([{ amount, userId: sessionUser.id }]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAmountChange = (value: number) => {
|
const handleAmountChange = (value: number) => {
|
||||||
setAmount(value);
|
setAmount(value);
|
||||||
const firstUserId = payments[0]?.userId ?? session.user.id;
|
const firstUserId = payments[0]?.userId ?? sessionUser.id;
|
||||||
setPayments([{ amount: value, userId: firstUserId }]);
|
setPayments([{ amount: value, userId: firstUserId }]);
|
||||||
recalculateSplits();
|
recalculateSplits();
|
||||||
};
|
};
|
||||||
@ -119,7 +118,7 @@ function ExpenseForm({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ExpenseParticipants session={session} />
|
<ExpenseParticipants sessionUserId={sessionUser.id} />
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@ -169,7 +168,7 @@ function ExpenseForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<ExpenseSplit session={session} />
|
<ExpenseSplit sessionUser={sessionUser} />
|
||||||
|
|
||||||
{/* <Tabs defaultValue="expense" className="size-full space-y-4">
|
{/* <Tabs defaultValue="expense" className="size-full space-y-4">
|
||||||
<TabsList className="w-full bg-transparent p-0">
|
<TabsList className="w-full bg-transparent p-0">
|
||||||
|
|||||||
@ -6,9 +6,30 @@ import FriendSelect from "../friend/friend-select";
|
|||||||
import { api } from "@/trpc/react";
|
import { api } from "@/trpc/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import type { Session } from "next-auth";
|
import type { User } from "@/server/db/schema";
|
||||||
|
|
||||||
export default function ExpenseParticipants({ session }: { session: Session }) {
|
export const UserBadge = ({
|
||||||
|
user,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
user: User;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="border rounded-full gap-1 flex items-center w-max pr-1">
|
||||||
|
<Avatar src={user.image} fb={user.name} className="size-6" />
|
||||||
|
<span className="text-sm">{user.name}</span>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ExpenseParticipants({
|
||||||
|
sessionUserId,
|
||||||
|
}: {
|
||||||
|
sessionUserId: string;
|
||||||
|
}) {
|
||||||
const [friends] = api.friend.getAll.useSuspenseQuery();
|
const [friends] = api.friend.getAll.useSuspenseQuery();
|
||||||
const participants = useExpenseStore((state) => state.participants);
|
const participants = useExpenseStore((state) => state.participants);
|
||||||
const addParticipant = useExpenseStore((state) => state.addParticipant);
|
const addParticipant = useExpenseStore((state) => state.addParticipant);
|
||||||
@ -30,14 +51,9 @@ export default function ExpenseParticipants({ session }: { session: Session }) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{participants.map((user) => (
|
{participants.map((user) => (
|
||||||
<ul
|
<ul key={user.id}>
|
||||||
key={user.id}
|
<UserBadge user={user}>
|
||||||
className="border rounded-full gap-1 flex items-center w-max "
|
{sessionUserId !== user.id && (
|
||||||
>
|
|
||||||
<Avatar src={user.image} fb={user.name} className="size-6" />
|
|
||||||
|
|
||||||
<span className="pr-4 text-sm">{user.name}</span>
|
|
||||||
{session.user.id !== user.id && (
|
|
||||||
<Button
|
<Button
|
||||||
className=" rounded-full aspect-square size-6"
|
className=" rounded-full aspect-square size-6"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@ -49,6 +65,7 @@ export default function ExpenseParticipants({ session }: { session: Session }) {
|
|||||||
<XIcon className="size-4" />
|
<XIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</UserBadge>
|
||||||
</ul>
|
</ul>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,15 +3,14 @@ import React from "react";
|
|||||||
import FriendSelect from "../friend/friend-select";
|
import FriendSelect from "../friend/friend-select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import type { Session } from "next-auth";
|
|
||||||
|
|
||||||
import { Icons } from "@/components/icons";
|
import { Icons } from "@/components/icons";
|
||||||
import { useExpenseStore } from "@/lib/store/expense-store";
|
import { useExpenseStore } from "@/lib/store/expense-store";
|
||||||
import PaidByInput from "./paid-by";
|
import PaidByInput from "./paid-by";
|
||||||
import { api } from "@/trpc/react";
|
import { api } from "@/trpc/react";
|
||||||
import SplitTo from "./split-to";
|
import SplitTo from "./split-to";
|
||||||
|
import type { User } from "@/server/db/schema";
|
||||||
|
|
||||||
export default function ExpenseSplit({ session }: { session: Session }) {
|
export default function ExpenseSplit({ sessionUser }: { sessionUser: User }) {
|
||||||
const [friends] = api.friend.getAll.useSuspenseQuery();
|
const [friends] = api.friend.getAll.useSuspenseQuery();
|
||||||
|
|
||||||
const friendTarget = useExpenseStore((state) => state.friendTarget);
|
const friendTarget = useExpenseStore((state) => state.friendTarget);
|
||||||
@ -22,7 +21,7 @@ export default function ExpenseSplit({ session }: { session: Session }) {
|
|||||||
<div className="space-y-4 ">
|
<div className="space-y-4 ">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<span>Paid by</span>
|
<span>Paid by</span>
|
||||||
<PaidByInput sessionUser={session.user} />
|
<PaidByInput sessionUser={sessionUser} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<span>and spliited</span>
|
<span>and spliited</span>
|
||||||
|
|||||||
@ -8,20 +8,16 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DialogClose } from "@radix-ui/react-dialog";
|
import { DialogClose } from "@radix-ui/react-dialog";
|
||||||
import type { PublicUser } from "next-auth";
|
|
||||||
import Avatar from "@/components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
import { Icons } from "@/components/icons";
|
import { Icons } from "@/components/icons";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import PaidByCustomSplit from "./paid-by-custom-split";
|
import PaidByCustomSplit from "./paid-by-custom-split";
|
||||||
import { useExpenseStore } from "@/lib/store/expense-store";
|
import { useExpenseStore } from "@/lib/store/expense-store";
|
||||||
|
import type { User } from "@/server/db/schema";
|
||||||
|
|
||||||
/* saved result is an array of objects with user and amount */
|
/* saved result is an array of objects with user and amount */
|
||||||
export default function PaidByInput({
|
export default function PaidByInput({ sessionUser }: { sessionUser: User }) {
|
||||||
sessionUser,
|
|
||||||
}: {
|
|
||||||
sessionUser: PublicUser;
|
|
||||||
}) {
|
|
||||||
const amount = useExpenseStore((state) => state.amount);
|
const amount = useExpenseStore((state) => state.amount);
|
||||||
const payments = useExpenseStore((state) => state.payments);
|
const payments = useExpenseStore((state) => state.payments);
|
||||||
const setPayments = useExpenseStore((state) => state.setPayments);
|
const setPayments = useExpenseStore((state) => state.setPayments);
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
// "use client";
|
|
||||||
// import React, { useEffect, useState } from "react";
|
|
||||||
// export function usePwaInstallStatus() {
|
|
||||||
// const [isStandalone, setIsStandalone] = useState(false);
|
|
||||||
// const [isIOS, setIsIOS] = useState(false);
|
|
||||||
// const [isAndroid, setIsAndroid] = useState(false);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const userAgent =
|
|
||||||
// navigator.userAgent || navigator.vendor || (window as any).opera;
|
|
||||||
|
|
||||||
// // iOS detection
|
|
||||||
// const iOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
|
|
||||||
// setIsIOS(iOS);
|
|
||||||
|
|
||||||
// // Android detection
|
|
||||||
// const android = /android/i.test(userAgent);
|
|
||||||
// setIsAndroid(android);
|
|
||||||
|
|
||||||
// // PWA "standalone" mode detection (works for both iOS & Android)
|
|
||||||
// const isInStandaloneMode =
|
|
||||||
// window.matchMedia("(display-mode: standalone)").matches ||
|
|
||||||
// (navigator as any).standalone === true; // for iOS Safari
|
|
||||||
|
|
||||||
// setIsStandalone(isInStandaloneMode);
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// return { isIOS, isAndroid, isStandalone };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function InstallPrompt() {
|
|
||||||
// const { isIOS, isAndroid, isStandalone } = usePwaInstallStatus();
|
|
||||||
// const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const handler = (e: any) => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// setDeferredPrompt(e);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// window.addEventListener("beforeinstallprompt", handler);
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// window.removeEventListener("beforeinstallprompt", handler);
|
|
||||||
// };
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// const handleInstallClick = () => {
|
|
||||||
// console.log("install clicked");
|
|
||||||
|
|
||||||
// if (deferredPrompt) {
|
|
||||||
// deferredPrompt.prompt();
|
|
||||||
// deferredPrompt.userChoice.then((choiceResult: any) => {
|
|
||||||
// if (choiceResult.outcome === "accepted") {
|
|
||||||
// console.log("User accepted the A2HS prompt");
|
|
||||||
// } else {
|
|
||||||
// console.log("User dismissed the A2HS prompt");
|
|
||||||
// }
|
|
||||||
// setDeferredPrompt(null);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// if (isStandalone) {
|
|
||||||
// return null; // Already running as a PWA, no need to show install prompt
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div className="absolute w-full h-20 z-50 bg-red-500 left-0 flex items-center justify-center right-0 bottom-0 text-white">
|
|
||||||
// {isIOS && <p>Tap "Share" and "Add to Home Screen" to install the app.</p>}
|
|
||||||
// {isAndroid && <button onClick={handleInstallClick}>Install App</button>}
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default function PWAWrapper({
|
|
||||||
// children,
|
|
||||||
// }: {
|
|
||||||
// children: React.ReactNode;
|
|
||||||
// }) {
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// <InstallPrompt />
|
|
||||||
// {children}
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import type { User } from "next-auth";
|
|
||||||
import Avatar from "@/components/avatar";
|
|
||||||
import { LogOut } from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Icons } from "@/components/icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default function UserDropdown({ user }: { user: User }) {
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<Avatar fb={user.name} src={user.image} />
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
<DropdownMenuItem className="justify-between " asChild>
|
|
||||||
<Link href={"/me"}>
|
|
||||||
Profile
|
|
||||||
<Icons.user className="size-4 text-foreground" />
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem className="justify-between " asChild>
|
|
||||||
<Link href={"/billing"}>
|
|
||||||
Billing
|
|
||||||
<Icons.wallet className="size-4 text-foreground" />
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem className="justify-between text-destructive" asChild>
|
|
||||||
<Link href={"/api/auth/signout"}>
|
|
||||||
Logout
|
|
||||||
<Icons.logout className="size-4 text-destructive" />
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { handlers } from "@/server/auth";
|
|
||||||
|
|
||||||
export const { GET, POST } = handlers;
|
|
||||||
43
src/app/api/webhooks/route.ts
Normal file
43
src/app/api/webhooks/route.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { env } from "@/env";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { users } from "@/server/db/schema";
|
||||||
|
import { verifyWebhook } from "@clerk/nextjs/webhooks";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const evt = await verifyWebhook(req, {
|
||||||
|
signingSecret: env.CLERK_WEBHOOK_SIGNING_SECRET,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventType = evt.type;
|
||||||
|
switch (eventType) {
|
||||||
|
case "user.created":
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: evt.data.id,
|
||||||
|
name: evt.data.first_name ?? evt.data.last_name,
|
||||||
|
image: evt.data.image_url,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "user.updated":
|
||||||
|
await db
|
||||||
|
.update(users)
|
||||||
|
.set({
|
||||||
|
name: evt.data.first_name ?? evt.data.last_name,
|
||||||
|
image: evt.data.image_url,
|
||||||
|
})
|
||||||
|
.where(eq(users.id, evt.data.id));
|
||||||
|
break;
|
||||||
|
case "user.deleted":
|
||||||
|
await db.delete(users).where(eq(users.id, evt.data.id!));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Unknown event type:", eventType);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return new Response("Webhook received", { status: 200 });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error verifying webhook:", err);
|
||||||
|
return new Response("Error verifying webhook", { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,8 +3,7 @@ import "@/styles/globals.css";
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist } from "next/font/google";
|
import { Geist } from "next/font/google";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { TRPCReactProvider } from "@/trpc/react";
|
import Providers from "./providers";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
|
||||||
// import PWAWrapper from "./_components/install-prompt";
|
// import PWAWrapper from "./_components/install-prompt";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@ -24,19 +23,10 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en" className={`${geist.variable}`}>
|
<html lang="en" className={`${geist.variable}`}>
|
||||||
<body>
|
<body>
|
||||||
<TRPCReactProvider>
|
<Providers>
|
||||||
<ThemeProvider
|
|
||||||
attribute="class"
|
|
||||||
defaultTheme="system"
|
|
||||||
enableSystem
|
|
||||||
disableTransitionOnChange
|
|
||||||
>
|
|
||||||
{/* <PWAWrapper>
|
|
||||||
</PWAWrapper> */}
|
|
||||||
{children}
|
{children}
|
||||||
<Toaster position="top-center" />
|
<Toaster position="top-center" />
|
||||||
</ThemeProvider>
|
</Providers>
|
||||||
</TRPCReactProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
32
src/app/providers.tsx
Normal file
32
src/app/providers.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { TRPCReactProvider } from "@/trpc/react";
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
|
||||||
|
import { ClerkProvider } from "@clerk/nextjs";
|
||||||
|
import { dark } from "@clerk/themes";
|
||||||
|
|
||||||
|
function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ClerkProvider
|
||||||
|
appearance={{
|
||||||
|
baseTheme: dark,
|
||||||
|
elements: {
|
||||||
|
userButtonAvatarBox: { height: "2rem", width: "2rem" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TRPCReactProvider>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
</TRPCReactProvider>
|
||||||
|
</ClerkProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Providers;
|
||||||
@ -12,9 +12,12 @@ export default function Header({
|
|||||||
return (
|
return (
|
||||||
<div className="w-full p-4 border-b flex justify-between items-center relative">
|
<div className="w-full p-4 border-b flex justify-between items-center relative">
|
||||||
<h2 className="font-black text-2xl uppercase">{text}</h2>
|
<h2 className="font-black text-2xl uppercase">{text}</h2>
|
||||||
<div className="glow absolute right-0 top-0 w-20 h-8 bg-primary blur-2xl -z-10" />
|
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
{/* Glow effect */}
|
||||||
|
{glow && (
|
||||||
|
<div className="glow absolute right-0 top-0 w-20 h-8 blur-2xl -z-10 bg-gradient-to-bl from-primary to-transparent" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ type IconName =
|
|||||||
| "logo"
|
| "logo"
|
||||||
| "home"
|
| "home"
|
||||||
| "wallet"
|
| "wallet"
|
||||||
|
| "brokenWallet"
|
||||||
| "add"
|
| "add"
|
||||||
| "group"
|
| "group"
|
||||||
| "friends"
|
| "friends"
|
||||||
@ -81,6 +82,23 @@ export const Icons: Record<IconName, IconComponent> = {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
brokenWallet(props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.18465 3.97712L9.16135 13.118C9.16869 13.1406 9.177 13.1626 9.18622 13.1839V15.2C9.18622 15.6142 9.52201 15.95 9.93622 15.95C10.3504 15.95 10.6862 15.6142 10.6862 15.2V3.5817C10.9817 3.45057 11.3028 3.37548 11.6418 3.32991C12.2366 3.24995 12.9858 3.24997 13.8842 3.25L18.4918 3.25C21.3606 3.25 23.6863 5.57564 23.6863 8.44445C23.6863 9.27618 23.2093 10.0342 22.4595 10.3941L21.9721 10.6281C20.8211 11.1806 20.8211 12.8194 21.9721 13.3719L22.4595 13.6059C23.2093 13.9658 23.6863 14.7238 23.6863 15.5556C23.6863 18.4244 21.3606 20.75 18.4918 20.75H9.93793C10.3514 20.7491 10.6862 20.4136 10.6862 20V18.8C10.6862 18.3858 10.3504 18.05 9.93622 18.05C9.52201 18.05 9.18622 18.3858 9.18622 18.8V20C9.18622 20.4134 9.5207 20.7487 9.9338 20.75L6.81991 20.75C5.07675 20.75 3.46745 19.8153 2.60378 18.3011L2.41643 17.9726C2.33486 17.8296 2.272 17.6767 2.22939 17.5177C2.03805 16.8036 2.27319 16.043 2.83417 15.5615L3.35914 15.1109C4.32795 14.2793 3.90377 12.6963 2.64897 12.4605L2.11766 12.3607C1.30023 12.2071 0.643309 11.5984 0.428041 10.795C-0.314462 8.02393 1.33001 5.17562 4.10107 4.43312L4.13954 4.42281C4.81066 4.24297 5.37063 4.09291 5.8359 4.01936C5.9532 4.00082 6.06938 3.98623 6.18465 3.97712Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
},
|
||||||
add(props) {
|
add(props) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@ -38,8 +38,10 @@ function NavLink({ href, name, icon }: NavLink) {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Glow effect */}
|
||||||
{active && (
|
{active && (
|
||||||
<div className="h-16 w-16 left-1/2 -z-10 -translate-x-1/2 transform blur-3xl absolute -bottom-8 bg-primary" />
|
<div className="size-16 left-1/2 -z-10 -translate-x-1/2 transform blur-3xl absolute -bottom-8 bg-gradient-to-t from-primary to-transparent" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
26
src/env.js
26
src/env.js
@ -7,16 +7,18 @@ export const env = createEnv({
|
|||||||
* isn't built with invalid env vars.
|
* isn't built with invalid env vars.
|
||||||
*/
|
*/
|
||||||
server: {
|
server: {
|
||||||
AUTH_SECRET:
|
// AUTH_SECRET:
|
||||||
process.env.NODE_ENV === "production"
|
// process.env.NODE_ENV === "production"
|
||||||
? z.string()
|
// ? z.string()
|
||||||
: z.string().optional(),
|
// : z.string().optional(),
|
||||||
AUTH_DISCORD_ID: z.string(),
|
// AUTH_DISCORD_ID: z.string(),
|
||||||
AUTH_DISCORD_SECRET: z.string(),
|
// AUTH_DISCORD_SECRET: z.string(),
|
||||||
DATABASE_URL: z.string().url(),
|
DATABASE_URL: z.string().url(),
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["development", "test", "production"])
|
.enum(["development", "test", "production"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
|
CLERK_SECRET_KEY: z.string(),
|
||||||
|
CLERK_WEBHOOK_SIGNING_SECRET: z.string(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,6 +28,7 @@ export const env = createEnv({
|
|||||||
*/
|
*/
|
||||||
client: {
|
client: {
|
||||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||||
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,9 +36,14 @@ export const env = createEnv({
|
|||||||
* middlewares) or client-side so we need to destruct manually.
|
* middlewares) or client-side so we need to destruct manually.
|
||||||
*/
|
*/
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
AUTH_SECRET: process.env.AUTH_SECRET,
|
// AUTH_SECRET: process.env.AUTH_SECRET,
|
||||||
AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
|
// AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
|
||||||
AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
|
// AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
|
||||||
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
|
||||||
|
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
||||||
|
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
||||||
|
CLERK_WEBHOOK_SIGNING_SECRET: process.env.CLERK_WEBHOOK_SIGNING_SECRET,
|
||||||
|
|
||||||
DATABASE_URL: process.env.DATABASE_URL,
|
DATABASE_URL: process.env.DATABASE_URL,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -16,3 +16,5 @@ export function debounce<T extends (...args: any[]) => void>(
|
|||||||
timeoutId = setTimeout(() => func(...args), delay);
|
timeoutId = setTimeout(() => func(...args), delay);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAmount = (amount: number) => `${amount.toFixed(2)}€`;
|
||||||
|
|||||||
21
src/middleware.ts
Normal file
21
src/middleware.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
|
||||||
|
|
||||||
|
const isProtectedRoute = createRouteMatcher([
|
||||||
|
"/",
|
||||||
|
"/friend",
|
||||||
|
"/expense",
|
||||||
|
"/group",
|
||||||
|
"/add",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default clerkMiddleware(async (auth, req) => {
|
||||||
|
if (isProtectedRoute(req)) await auth.protect();
|
||||||
|
});
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// Skip Next.js internals and all static files, unless found in search params
|
||||||
|
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
||||||
|
// Always run for API routes
|
||||||
|
"/(api|trpc)(.*)",
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { expenseRouter } from "@/server/api/routers/expense";
|
|
||||||
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||||
import { friendRouter } from "./routers/friend";
|
import { expenseRouter, friendRouter, userRouter } from "./routers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@ -10,6 +9,7 @@ import { friendRouter } from "./routers/friend";
|
|||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
expense: expenseRouter,
|
expense: expenseRouter,
|
||||||
friend: friendRouter,
|
friend: friendRouter,
|
||||||
|
user: userRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@ -5,17 +5,41 @@ import { expenses, expenseSplits, type ExpenseSplit } from "@/server/db/schema";
|
|||||||
import { expenseSchema, expenseSplitSchema } from "@/lib/validations/expense";
|
import { expenseSchema, expenseSplitSchema } from "@/lib/validations/expense";
|
||||||
import { eq, or } from "drizzle-orm";
|
import { eq, or } from "drizzle-orm";
|
||||||
|
|
||||||
|
function restructureExpenseSplits(expenseSplits: Array<ExpenseSplit>) {
|
||||||
|
const expensesMap = new Map();
|
||||||
|
|
||||||
|
expenseSplits.forEach((split) => {
|
||||||
|
const expenseId = split.expenseId;
|
||||||
|
const expense = split.expense;
|
||||||
|
|
||||||
|
if (!expensesMap.has(expenseId)) {
|
||||||
|
expensesMap.set(expenseId, {
|
||||||
|
...expense,
|
||||||
|
splits: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expensesMap.get(expenseId).splits.push(split);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(expensesMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
export const expenseRouter = createTRPCRouter({
|
export const expenseRouter = createTRPCRouter({
|
||||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return await ctx.db.query.expenseSplits.findMany({
|
const splits = await ctx.db.query.expenseSplits.findMany({
|
||||||
where: or(
|
where: or(
|
||||||
eq(expenseSplits.owedFromId, ctx.session.user.id),
|
eq(expenseSplits.owedFromId, ctx.auth.userId),
|
||||||
eq(expenseSplits.owedToId, ctx.session.user.id)
|
eq(expenseSplits.owedToId, ctx.auth.userId)
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
|
owedFrom: true,
|
||||||
|
owedTo: true,
|
||||||
expense: true,
|
expense: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const expenses = restructureExpenseSplits(splits);
|
||||||
|
return expenses;
|
||||||
}),
|
}),
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
@ -34,7 +58,7 @@ export const expenseRouter = createTRPCRouter({
|
|||||||
const [expense] = await ctx.db
|
const [expense] = await ctx.db
|
||||||
.insert(expenses)
|
.insert(expenses)
|
||||||
.values({
|
.values({
|
||||||
createdById: ctx.session.user.id,
|
createdById: ctx.auth.userId,
|
||||||
...input.expense,
|
...input.expense,
|
||||||
amount: input.expense.amount.toString(),
|
amount: input.expense.amount.toString(),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,8 +10,8 @@ export const friendRouter = createTRPCRouter({
|
|||||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const friends = await ctx.db.query.friendships.findMany({
|
const friends = await ctx.db.query.friendships.findMany({
|
||||||
where: or(
|
where: or(
|
||||||
eq(friendships.userOneId, ctx.session.user.id),
|
eq(friendships.userOneId, ctx.auth.userId),
|
||||||
eq(friendships.userTwoId, ctx.session.user.id)
|
eq(friendships.userTwoId, ctx.auth.userId)
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
userOne: true,
|
userOne: true,
|
||||||
@ -21,8 +21,8 @@ export const friendRouter = createTRPCRouter({
|
|||||||
return friends.map((f) => ({
|
return friends.map((f) => ({
|
||||||
id: f.id,
|
id: f.id,
|
||||||
status: f.status,
|
status: f.status,
|
||||||
requestedBy: ctx.session.user.id === f.userOneId ? "me" : "them",
|
requestedBy: ctx.auth.userId === f.userOneId ? "me" : "them",
|
||||||
user: ctx.session.user.id === f.userOneId ? f.userTwo : f.userOne,
|
user: ctx.auth.userId === f.userOneId ? f.userTwo : f.userOne,
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
getPendingFriendRequests: protectedProcedure.query(
|
getPendingFriendRequests: protectedProcedure.query(
|
||||||
@ -30,8 +30,8 @@ export const friendRouter = createTRPCRouter({
|
|||||||
await ctx.db.query.friendships.findMany({
|
await ctx.db.query.friendships.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
or(
|
or(
|
||||||
eq(friendships.userOneId, ctx.session.user.id),
|
eq(friendships.userOneId, ctx.auth.userId),
|
||||||
eq(friendships.userTwoId, ctx.session.user.id)
|
eq(friendships.userTwoId, ctx.auth.userId)
|
||||||
),
|
),
|
||||||
eq(friendships.status, "pending")
|
eq(friendships.status, "pending")
|
||||||
),
|
),
|
||||||
@ -40,7 +40,7 @@ export const friendRouter = createTRPCRouter({
|
|||||||
search: protectedProcedure
|
search: protectedProcedure
|
||||||
.input(z.object({ search: z.string() }))
|
.input(z.object({ search: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const userId = ctx.session.user.id;
|
const userId = ctx.auth.userId;
|
||||||
const friendIds = await ctx.db.query.friendships.findMany({
|
const friendIds = await ctx.db.query.friendships.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
or(
|
or(
|
||||||
@ -91,7 +91,7 @@ export const friendRouter = createTRPCRouter({
|
|||||||
.input(z.object({ userId: z.string() }))
|
.input(z.object({ userId: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await ctx.db.insert(friendships).values({
|
await ctx.db.insert(friendships).values({
|
||||||
userOneId: ctx.session.user.id,
|
userOneId: ctx.auth.userId,
|
||||||
userTwoId: input.userId,
|
userTwoId: input.userId,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
});
|
});
|
||||||
|
|||||||
3
src/server/api/routers/index.ts
Normal file
3
src/server/api/routers/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { friendRouter } from "./friend";
|
||||||
|
export { expenseRouter } from "./expense";
|
||||||
|
export { userRouter } from "./user";
|
||||||
@ -1,14 +1,12 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { users } from "@/server/db/schema";
|
||||||
|
|
||||||
export const userRouter = createTRPCRouter({
|
export const userRouter = createTRPCRouter({
|
||||||
getAll: protectedProcedure.query(
|
getSessionUser: protectedProcedure.query(async ({ ctx }) => {
|
||||||
async ({ ctx }) =>
|
const user = await ctx.db.query.users.findFirst({
|
||||||
await ctx.db.query.users.findMany({
|
where: eq(users.id, ctx.auth.userId),
|
||||||
columns: {
|
});
|
||||||
id: true,
|
return user;
|
||||||
name: true,
|
}),
|
||||||
image: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import { TRPCError, initTRPC } from "@trpc/server";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import { auth } from "@/server/auth";
|
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
|
import { auth } from "@clerk/nextjs/server";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
@ -27,11 +27,11 @@ import { db } from "@/server/db";
|
|||||||
* @see https://trpc.io/docs/server/context
|
* @see https://trpc.io/docs/server/context
|
||||||
*/
|
*/
|
||||||
export const createTRPCContext = async (opts: { headers: Headers }) => {
|
export const createTRPCContext = async (opts: { headers: Headers }) => {
|
||||||
const session = await auth();
|
const clerkAuth = await auth();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
db,
|
db,
|
||||||
session,
|
auth: clerkAuth,
|
||||||
...opts,
|
...opts,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -121,13 +121,13 @@ export const publicProcedure = t.procedure.use(timingMiddleware);
|
|||||||
export const protectedProcedure = t.procedure
|
export const protectedProcedure = t.procedure
|
||||||
.use(timingMiddleware)
|
.use(timingMiddleware)
|
||||||
.use(({ ctx, next }) => {
|
.use(({ ctx, next }) => {
|
||||||
if (!ctx.session?.user) {
|
if (!ctx.auth?.userId) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
return next({
|
return next({
|
||||||
ctx: {
|
ctx: {
|
||||||
// infers the `session` as non-nullable
|
// infers the `session` as non-nullable
|
||||||
session: { ...ctx.session, user: ctx.session.user },
|
auth: { ...ctx.auth, userId: ctx.auth.userId },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
|
||||||
import type { DefaultSession, NextAuthConfig, User } from "next-auth";
|
|
||||||
import DiscordProvider from "next-auth/providers/discord";
|
|
||||||
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
accounts,
|
|
||||||
sessions,
|
|
||||||
users,
|
|
||||||
verificationTokens,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
|
||||||
* object and keep type safety.
|
|
||||||
*
|
|
||||||
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
|
|
||||||
*/
|
|
||||||
declare module "next-auth" {
|
|
||||||
interface Session extends DefaultSession {
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
// ...other properties
|
|
||||||
// role: UserRole;
|
|
||||||
} & DefaultSession["user"];
|
|
||||||
}
|
|
||||||
|
|
||||||
// interface User {
|
|
||||||
// // ...other properties
|
|
||||||
// // role: UserRole;
|
|
||||||
// }
|
|
||||||
type PublicUser = Pick<User, "id" | "name" | "image">;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
|
|
||||||
*
|
|
||||||
* @see https://next-auth.js.org/configuration/options
|
|
||||||
*/
|
|
||||||
export const authConfig = {
|
|
||||||
trustHost: true,
|
|
||||||
providers: [
|
|
||||||
DiscordProvider,
|
|
||||||
/**
|
|
||||||
* ...add more providers here.
|
|
||||||
*
|
|
||||||
* Most other providers require a bit more work than the Discord provider. For example, the
|
|
||||||
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
|
|
||||||
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
|
|
||||||
*
|
|
||||||
* @see https://next-auth.js.org/providers/github
|
|
||||||
*/
|
|
||||||
],
|
|
||||||
adapter: DrizzleAdapter(db, {
|
|
||||||
usersTable: users,
|
|
||||||
accountsTable: accounts,
|
|
||||||
sessionsTable: sessions,
|
|
||||||
verificationTokensTable: verificationTokens,
|
|
||||||
}),
|
|
||||||
|
|
||||||
callbacks: {
|
|
||||||
session: ({ session, user }) => ({
|
|
||||||
...session,
|
|
||||||
user: {
|
|
||||||
...session.user,
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
} satisfies NextAuthConfig;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import NextAuth from "next-auth";
|
|
||||||
import { cache } from "react";
|
|
||||||
|
|
||||||
import { authConfig } from "./config";
|
|
||||||
|
|
||||||
const { auth: uncachedAuth, handlers, signIn, signOut } = NextAuth(authConfig);
|
|
||||||
|
|
||||||
const auth = cache(uncachedAuth);
|
|
||||||
|
|
||||||
export { auth, handlers, signIn, signOut };
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import { relations, sql } from "drizzle-orm";
|
import { relations, sql } from "drizzle-orm";
|
||||||
import { index, pgTableCreator, primaryKey } from "drizzle-orm/pg-core";
|
import { index, pgTableCreator } from "drizzle-orm/pg-core";
|
||||||
import type { User } from "next-auth";
|
import { createId as createCuid2 } from "@paralleldrive/cuid2";
|
||||||
import type { AdapterAccount } from "next-auth/adapters";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||||
* database instance for multiple projects.
|
* database instance for multiple projects.
|
||||||
@ -11,17 +9,16 @@ import type { AdapterAccount } from "next-auth/adapters";
|
|||||||
*/
|
*/
|
||||||
export const createTable = pgTableCreator((name) => `betterwise_${name}`);
|
export const createTable = pgTableCreator((name) => `betterwise_${name}`);
|
||||||
|
|
||||||
|
const createId = () => createCuid2();
|
||||||
|
|
||||||
export const expenses = createTable(
|
export const expenses = createTable(
|
||||||
"expense",
|
"expense",
|
||||||
(d) => ({
|
(d) => ({
|
||||||
id: d
|
id: d.varchar().primaryKey().$defaultFn(createId),
|
||||||
.uuid()
|
groupId: d.varchar(),
|
||||||
.primaryKey()
|
friendId: d.varchar(),
|
||||||
.default(sql`gen_random_uuid()`),
|
|
||||||
groupId: d.uuid(),
|
|
||||||
friendId: d.uuid(),
|
|
||||||
createdById: d
|
createdById: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
amount: d.numeric({ scale: 2 }),
|
amount: d.numeric({ scale: 2 }),
|
||||||
@ -61,20 +58,17 @@ export type Expense = typeof expenses.$inferSelect & {
|
|||||||
export const expenseSplits = createTable(
|
export const expenseSplits = createTable(
|
||||||
"expense_split",
|
"expense_split",
|
||||||
(d) => ({
|
(d) => ({
|
||||||
id: d
|
id: d.varchar().primaryKey().$defaultFn(createId),
|
||||||
.uuid()
|
|
||||||
.primaryKey()
|
|
||||||
.default(sql`gen_random_uuid()`),
|
|
||||||
expenseId: d
|
expenseId: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => expenses.id, { onDelete: "cascade" }),
|
.references(() => expenses.id, { onDelete: "cascade" }),
|
||||||
owedToId: d
|
owedToId: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
owedFromId: d
|
owedFromId: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
amount: d.numeric({ scale: 2 }),
|
amount: d.numeric({ scale: 2 }),
|
||||||
@ -105,22 +99,20 @@ export const expenseSplitRelations = relations(expenseSplits, ({ one }) => ({
|
|||||||
export type ExpenseSplit = typeof expenseSplits.$inferSelect & {
|
export type ExpenseSplit = typeof expenseSplits.$inferSelect & {
|
||||||
expense?: Expense;
|
expense?: Expense;
|
||||||
paidBy?: User;
|
paidBy?: User;
|
||||||
owedBy?: User;
|
owedTo?: User;
|
||||||
|
owedFrom?: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const settlements = createTable(
|
export const settlements = createTable(
|
||||||
"settlement",
|
"settlement",
|
||||||
(d) => ({
|
(d) => ({
|
||||||
id: d
|
id: d.varchar().primaryKey().$defaultFn(createId),
|
||||||
.uuid()
|
|
||||||
.primaryKey()
|
|
||||||
.default(sql`gen_random_uuid()`),
|
|
||||||
payerId: d
|
payerId: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
receiverId: d
|
receiverId: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
amount: d.numeric().notNull(),
|
amount: d.numeric().notNull(),
|
||||||
@ -153,18 +145,16 @@ export type Settlement = typeof settlements.$inferSelect & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Groups Table
|
// Groups Table
|
||||||
|
|
||||||
export const groups = createTable(
|
export const groups = createTable(
|
||||||
"group",
|
"group",
|
||||||
(d) => ({
|
(d) => ({
|
||||||
id: d
|
id: d.varchar().primaryKey().$defaultFn(createId),
|
||||||
.uuid()
|
|
||||||
.primaryKey()
|
|
||||||
.default(sql`gen_random_uuid()`),
|
|
||||||
name: d.varchar({ length: 255 }).notNull(),
|
name: d.varchar({ length: 255 }).notNull(),
|
||||||
createdById: d
|
createdById: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: "cascade" }),
|
.references(() => users.id),
|
||||||
createdAt: d
|
createdAt: d
|
||||||
.timestamp({ withTimezone: true })
|
.timestamp({ withTimezone: true })
|
||||||
.default(sql`CURRENT_TIMESTAMP`)
|
.default(sql`CURRENT_TIMESTAMP`)
|
||||||
@ -190,16 +180,13 @@ export type Group = typeof groups.$inferSelect & {
|
|||||||
export const groupMembers = createTable(
|
export const groupMembers = createTable(
|
||||||
"group_member",
|
"group_member",
|
||||||
(d) => ({
|
(d) => ({
|
||||||
id: d
|
id: d.varchar().primaryKey().$defaultFn(createId),
|
||||||
.uuid()
|
|
||||||
.primaryKey()
|
|
||||||
.default(sql`gen_random_uuid()`),
|
|
||||||
groupId: d
|
groupId: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => groups.id, { onDelete: "cascade" }),
|
.references(() => groups.id, { onDelete: "cascade" }),
|
||||||
userId: d
|
userId: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
joinedAt: d
|
joinedAt: d
|
||||||
@ -233,19 +220,17 @@ export type GroupMember = typeof groupMembers.$inferSelect & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Friendships Table (Tracks friend relationships)
|
// Friendships Table (Tracks friend relationships)
|
||||||
|
|
||||||
export const friendships = createTable(
|
export const friendships = createTable(
|
||||||
"friendship",
|
"friendship",
|
||||||
(d) => ({
|
(d) => ({
|
||||||
id: d
|
id: d.varchar().primaryKey().$defaultFn(createId),
|
||||||
.uuid()
|
|
||||||
.primaryKey()
|
|
||||||
.default(sql`gen_random_uuid()`),
|
|
||||||
userOneId: d
|
userOneId: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
userTwoId: d
|
userTwoId: d
|
||||||
.uuid()
|
.varchar()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
status: d
|
status: d
|
||||||
@ -281,76 +266,11 @@ export type Friendship = typeof friendships.$inferSelect & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const users = createTable("user", (d) => ({
|
export const users = createTable("user", (d) => ({
|
||||||
id: d
|
id: d.varchar().notNull().primaryKey().unique(),
|
||||||
.uuid()
|
|
||||||
.primaryKey()
|
|
||||||
.default(sql`gen_random_uuid()`),
|
|
||||||
name: d.varchar({ length: 255 }),
|
name: d.varchar({ length: 255 }),
|
||||||
email: d.varchar({ length: 255 }).notNull(),
|
|
||||||
emailVerified: d
|
|
||||||
.timestamp({
|
|
||||||
mode: "date",
|
|
||||||
withTimezone: true,
|
|
||||||
})
|
|
||||||
.default(sql`CURRENT_TIMESTAMP`),
|
|
||||||
image: d.varchar({ length: 255 }),
|
image: d.varchar({ length: 255 }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const usersRelations = relations(users, ({ many }) => ({
|
export type User = typeof users.$inferSelect;
|
||||||
accounts: many(accounts),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const accounts = createTable(
|
// export const usersRelations = relations(users, ({ many }) => ({}));
|
||||||
"account",
|
|
||||||
(d) => ({
|
|
||||||
userId: d
|
|
||||||
.uuid()
|
|
||||||
.notNull()
|
|
||||||
.references(() => users.id),
|
|
||||||
type: d.varchar({ length: 255 }).$type<AdapterAccount["type"]>().notNull(),
|
|
||||||
provider: d.varchar({ length: 255 }).notNull(),
|
|
||||||
providerAccountId: d.varchar({ length: 255 }).notNull(),
|
|
||||||
refresh_token: d.text(),
|
|
||||||
access_token: d.text(),
|
|
||||||
expires_at: d.integer(),
|
|
||||||
token_type: d.varchar({ length: 255 }),
|
|
||||||
scope: d.varchar({ length: 255 }),
|
|
||||||
id_token: d.text(),
|
|
||||||
session_state: d.varchar({ length: 255 }),
|
|
||||||
}),
|
|
||||||
(t) => [
|
|
||||||
primaryKey({ columns: [t.provider, t.providerAccountId] }),
|
|
||||||
index("account_user_id_idx").on(t.userId),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
export const accountsRelations = relations(accounts, ({ one }) => ({
|
|
||||||
user: one(users, { fields: [accounts.userId], references: [users.id] }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const sessions = createTable(
|
|
||||||
"session",
|
|
||||||
(d) => ({
|
|
||||||
sessionToken: d.varchar({ length: 255 }).notNull().primaryKey(),
|
|
||||||
userId: d
|
|
||||||
.uuid()
|
|
||||||
.notNull()
|
|
||||||
.references(() => users.id),
|
|
||||||
expires: d.timestamp({ mode: "date", withTimezone: true }).notNull(),
|
|
||||||
}),
|
|
||||||
(t) => [index("t_user_id_idx").on(t.userId)]
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sessionsRelations = relations(sessions, ({ one }) => ({
|
|
||||||
user: one(users, { fields: [sessions.userId], references: [users.id] }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const verificationTokens = createTable(
|
|
||||||
"verification_token",
|
|
||||||
(d) => ({
|
|
||||||
identifier: d.varchar({ length: 255 }).notNull(),
|
|
||||||
token: d.varchar({ length: 255 }).notNull(),
|
|
||||||
expires: d.timestamp({ mode: "date", withTimezone: true }).notNull(),
|
|
||||||
}),
|
|
||||||
(t) => [primaryKey({ columns: [t.identifier, t.token] })]
|
|
||||||
);
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user