diff --git a/src/app/api/transfer/route.ts b/src/app/api/transfer/route.ts new file mode 100644 index 00000000..d369929b --- /dev/null +++ b/src/app/api/transfer/route.ts @@ -0,0 +1,75 @@ +import { NextResponse } from "next/server"; +import { auth } from "~/server/auth"; +import { db } from "~/server/db"; + +export async function POST(request: Request) { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { toUserId, amount } = (await request.json()) as { + toUserId: string; + amount: number; + }; + + if (!toUserId || typeof toUserId !== "string") { + return NextResponse.json({ error: "Invalid recipient" }, { status: 400 }); + } + + if (typeof amount !== "number" || isNaN(amount) || amount <= 0) { + return NextResponse.json( + { error: "Amount must be a positive number" }, + { status: 400 }, + ); + } + + const fromUserId = session.user.id; + + if (fromUserId === toUserId) { + return NextResponse.json( + { error: "Cannot transfer to yourself" }, + { status: 400 }, + ); + } + + const [sender, recipient] = await Promise.all([ + db.user.findUnique({ + where: { id: fromUserId }, + select: { id: true, balance: true }, + }), + db.user.findUnique({ + where: { id: toUserId }, + select: { id: true, name: true }, + }), + ]); + + if (!sender) { + return NextResponse.json({ error: "Sender not found" }, { status: 404 }); + } + + if (!recipient) { + return NextResponse.json({ error: "Recipient not found" }, { status: 404 }); + } + + if (sender.balance < amount) { + return NextResponse.json( + { error: "Insufficient balance" }, + { status: 400 }, + ); + } + + await db.$transaction([ + db.user.update({ + where: { id: fromUserId }, + data: { balance: { decrement: amount } }, + }), + db.user.update({ + where: { id: toUserId }, + data: { balance: { increment: amount } }, + }), + ]); + + return NextResponse.json({ success: true, amount, to: recipient.name }); +} diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts new file mode 100644 index 00000000..157810c1 --- /dev/null +++ b/src/app/api/users/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from "next/server"; +import { auth } from "~/server/auth"; +import { db } from "~/server/db"; + +export async function GET() { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const users = await db.user.findMany({ + where: { id: { not: session.user.id } }, + select: { id: true, name: true }, + orderBy: { name: "asc" }, + }); + + return NextResponse.json(users); +} diff --git a/src/components/HomeClient.tsx b/src/components/HomeClient.tsx index 20d1d411..4209dc01 100644 --- a/src/components/HomeClient.tsx +++ b/src/components/HomeClient.tsx @@ -8,6 +8,7 @@ import CartButton from "~/components/cart"; import type { Session } from "next-auth"; import AccountButton from "./account"; import SellableItemsButton from "./new_items"; +import TransferButton from "./transfer"; type UserResponse = { id: string; @@ -102,9 +103,10 @@ export default function HomeClient({ session }: Props) {
{userData && ( -
- ${userData.balance ?? 0} -
+ )} {session?.user && ( <> diff --git a/src/components/transfer.tsx b/src/components/transfer.tsx new file mode 100644 index 00000000..ed32c65e --- /dev/null +++ b/src/components/transfer.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useState } from "react"; + +type User = { + id: string; + name: string | null; +}; + +type Props = { + balance: number; + reloadUser: () => Promise; +}; + +export default function TransferButton({ balance, reloadUser }: Props) { + const [isOpen, setIsOpen] = useState(false); + const [users, setUsers] = useState([]); + const [toUserId, setToUserId] = useState(""); + const [amount, setAmount] = useState(""); + const [error, setError] = useState(null); + const [sending, setSending] = useState(false); + + const openModal = async () => { + setError(null); + setAmount(""); + setToUserId(""); + try { + const res = await fetch("/api/users"); + if (!res.ok) throw new Error("Failed to load users"); + const data = (await res.json()) as User[]; + setUsers(data); + if (data[0]) setToUserId(data[0].id); + setIsOpen(true); + } catch { + setError("Could not load users"); + } + }; + + const send = async () => { + setError(null); + const parsed = parseFloat(amount); + if (!toUserId) return setError("Select a recipient"); + if (isNaN(parsed) || parsed <= 0) return setError("Enter a valid amount"); + if (parsed > balance) return setError("Insufficient balance"); + + setSending(true); + try { + const res = await fetch("/api/transfer", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ toUserId, amount: parsed }), + }); + const data = (await res.json()) as { error?: string; to?: string }; + if (!res.ok) { + setError(data.error ?? "Transfer failed"); + return; + } + setIsOpen(false); + setAmount(""); + await reloadUser(); + } catch { + setError("Transfer failed"); + } finally { + setSending(false); + } + }; + + return ( + <> + + + {isOpen && ( +
+ {/* backdrop */} +
setIsOpen(false)} + /> + +
+

Transfer Balance

+ +
+ + +
+ +
+ + setAmount(e.target.value)} + placeholder="0" + className="w-full rounded bg-white/10 px-3 py-2 text-sm focus:outline-none" + /> +
+ + {error && ( +

+ {error} +

+ )} + +
+ + +
+
+
+ )} + + ); +}