From 8f720da1d153a69d0b78947de6fc0924ed004ef0 Mon Sep 17 00:00:00 2001 From: Nasir Nadaf <109416738+VisibleNasir@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:07:01 +0530 Subject: [PATCH 1/3] feat: refactor authentication flow and update user model with session token --- apps/merchant-app/app/layout.tsx | 15 +- apps/merchant/.eslintrc.js | 0 apps/merchant/components/provider.tsx | 0 apps/merchant/next-env.d.ts | 0 apps/merchant/next.config.js | 0 apps/merchant/postcss.config.js | 0 apps/merchant/tailwind.config.js | 0 apps/merchant/tsconfig.json | 0 apps/user-app/.env.example | 2 - apps/user-app/app/(dashboard)/layout.tsx | 3 + .../app/api/auth/[...nextauth]/route.ts | 2 +- apps/user-app/app/auth/signin/page.tsx | 8 +- apps/user-app/app/auth/signup/page.tsx | 91 +++++++-- apps/user-app/app/lib/auth.ts | 173 ++++++++++-------- packages/db/.env.example | 4 +- packages/db/.gitignore | 1 + .../20240324100733_add_merchant/migration.sql | 15 -- .../20250909182115_initial/migration.sql | 54 ------ .../migration.sql | 16 -- .../20251009095729_latest_one/migration.sql | 15 -- .../migration.sql | 2 - .../migration.sql | 10 - .../migration.sql | 23 --- .../migration.sql | 21 --- .../migration.sql | 13 -- .../migration.sql | 17 -- .../migration.sql | 22 --- .../migration.sql | 18 -- packages/db/prisma/schema.prisma | 4 +- packages/db/prisma/seed.ts | 19 +- 30 files changed, 196 insertions(+), 352 deletions(-) delete mode 100644 apps/merchant/.eslintrc.js delete mode 100644 apps/merchant/components/provider.tsx delete mode 100644 apps/merchant/next-env.d.ts delete mode 100644 apps/merchant/next.config.js delete mode 100644 apps/merchant/postcss.config.js delete mode 100644 apps/merchant/tailwind.config.js delete mode 100644 apps/merchant/tsconfig.json delete mode 100644 packages/db/prisma/migrations/20240324100733_add_merchant/migration.sql delete mode 100644 packages/db/prisma/migrations/20250909182115_initial/migration.sql delete mode 100644 packages/db/prisma/migrations/20250927073302_adds_p2p_transfer/migration.sql delete mode 100644 packages/db/prisma/migrations/20251009095729_latest_one/migration.sql delete mode 100644 packages/db/prisma/migrations/20251019174632_added_userpin/migration.sql delete mode 100644 packages/db/prisma/migrations/20251019180428_make_userpin_required/migration.sql delete mode 100644 packages/db/prisma/migrations/20251019201334_add_p2p_requests/migration.sql delete mode 100644 packages/db/prisma/migrations/20251019211214_add_bill_schedule/migration.sql delete mode 100644 packages/db/prisma/migrations/20251019222841_add_is_recurring_to_bill_schedule/migration.sql delete mode 100644 packages/db/prisma/migrations/20251024130651_add_payment_method_default/migration.sql delete mode 100644 packages/db/prisma/migrations/20251027112237_add_merchant_payment/migration.sql delete mode 100644 packages/db/prisma/migrations/20251027141927_add_upi_id_to_merchant/migration.sql diff --git a/apps/merchant-app/app/layout.tsx b/apps/merchant-app/app/layout.tsx index 88c8764..33821cb 100644 --- a/apps/merchant-app/app/layout.tsx +++ b/apps/merchant-app/app/layout.tsx @@ -4,7 +4,6 @@ import { Inter } from "next/font/google"; import { Providers } from "../provider"; import QueryProvider from "../components/QueryProvider"; - const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { @@ -17,16 +16,16 @@ export default function RootLayout({ children, }: { children: React.ReactNode; -}): JSX.Element { +}) { return ( - - - - {children} - + {/* ---- Session / Auth Provider (client) ---- */} + + + {/* ---- React-Query Provider (client) ---- */} + {children} ); -} +} \ No newline at end of file diff --git a/apps/merchant/.eslintrc.js b/apps/merchant/.eslintrc.js deleted file mode 100644 index e69de29..0000000 diff --git a/apps/merchant/components/provider.tsx b/apps/merchant/components/provider.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/apps/merchant/next-env.d.ts b/apps/merchant/next-env.d.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/merchant/next.config.js b/apps/merchant/next.config.js deleted file mode 100644 index e69de29..0000000 diff --git a/apps/merchant/postcss.config.js b/apps/merchant/postcss.config.js deleted file mode 100644 index e69de29..0000000 diff --git a/apps/merchant/tailwind.config.js b/apps/merchant/tailwind.config.js deleted file mode 100644 index e69de29..0000000 diff --git a/apps/merchant/tsconfig.json b/apps/merchant/tsconfig.json deleted file mode 100644 index e69de29..0000000 diff --git a/apps/user-app/.env.example b/apps/user-app/.env.example index 2bde002..db77e80 100644 --- a/apps/user-app/.env.example +++ b/apps/user-app/.env.example @@ -1,7 +1,5 @@ JWT_SECRET=test NEXTAUTH_URL=http://localhost:3001 -NEXT_PUBLIC_HDFC_REDIRECT_URL="https://netbanking.hdfcbank.com" -NEXT_PUBLIC_AXIS_REDIRECT_URL="https://www.axisbank.com" NEXTAUTH_SECRET="your-secret-here" RAZORPAY_KEY_ID=your-razorpay-key-id RAZORPAY_KEY_SECRET=your-razorpay-key-secret diff --git a/apps/user-app/app/(dashboard)/layout.tsx b/apps/user-app/app/(dashboard)/layout.tsx index c9c3cab..994ae46 100644 --- a/apps/user-app/app/(dashboard)/layout.tsx +++ b/apps/user-app/app/(dashboard)/layout.tsx @@ -9,6 +9,7 @@ import { import { TooltipProvider, } from '../../../../packages/ui/src/tooltip'; import { Sidebar, SidebarBody, SidebarLink, SignupBtn } from '../../../../packages/ui/src/sidebar'; + // Your Links Interface interface Links { label: string; @@ -23,6 +24,7 @@ const sidebarLinks: Links[] = [ { label: "P2P Transfer", href: "/p2p", icon: }, { label: "Bills", href: "/bills", icon: }, ]; + export default function Layout({ children, @@ -30,6 +32,7 @@ export default function Layout({ children: React.ReactNode; }): JSX.Element { const { data: session } = useSession(); + return (
diff --git a/apps/user-app/app/api/auth/[...nextauth]/route.ts b/apps/user-app/app/api/auth/[...nextauth]/route.ts index 1cf7c68..5da81a6 100644 --- a/apps/user-app/app/api/auth/[...nextauth]/route.ts +++ b/apps/user-app/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,5 @@ +import { authOptions } from "@/app/lib/auth" import NextAuth from "next-auth" -import { authOptions } from "../../../lib/auth" const handler = NextAuth(authOptions) diff --git a/apps/user-app/app/auth/signin/page.tsx b/apps/user-app/app/auth/signin/page.tsx index 8df357f..3a3e3b0 100644 --- a/apps/user-app/app/auth/signin/page.tsx +++ b/apps/user-app/app/auth/signin/page.tsx @@ -18,7 +18,7 @@ export default function SignInPage() { await signIn("credentials", { phone, password, - callbackUrl: "/", // Redirect after successful login + callbackUrl: "/", }); }; @@ -32,14 +32,14 @@ export default function SignInPage() {
setPhone(e.target.value)} className="w-full p-2 border rounded-md outline-none bg-zinc-700 " - placeholder="1234567890" + placeholder="Enter Number" required />
@@ -50,7 +50,7 @@ export default function SignInPage() { value={password} onChange={(e) => setPassword(e.target.value)} className="w-full p-2 border rounded-md outline-none bg-zinc-700 " - placeholder="••••••••" + placeholder="Enter password" required />
diff --git a/apps/user-app/app/auth/signup/page.tsx b/apps/user-app/app/auth/signup/page.tsx index f20eab2..c8d29e7 100644 --- a/apps/user-app/app/auth/signup/page.tsx +++ b/apps/user-app/app/auth/signup/page.tsx @@ -7,46 +7,87 @@ import { CardHeader, CardTitle, } from "../../../components/ui/card"; +import { signIn } from "next-auth/react"; export default function SignUpPage() { - const [phone, setPhone] = useState(""); + const [phoneNumber, setPhoneNumber] = useState(""); const [password, setPassword] = useState(""); + const [name , setName] = useState(""); + const [email , setEmail] = useState(""); + const [pin , setPin] = useState(""); + const [img , setImg] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const response = await fetch("/api/auth/signup", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ phone, password }), - }); + await signIn("credentials", { + phoneNumber, + password, + name, + email, + pin, + img, + callbackUrl: "/", + }) - if (response.ok) { - alert("Sign-up successful! Please sign in."); - } else { - alert("Sign-up failed."); - } }; return ( -
- - +
+ + Sign Up +
+ + setImg(e.target.value)} + className="p-2 border rounded-md hover:cursor-pointer" + /> +
+ + setName(e.target.value)} + className="w-full p-2 border rounded-md outline-none bg-zinc-700 text-white" + placeholder="Enter name" + required + /> +
+
+ setPhone(e.target.value)} + value={phoneNumber} + onChange={(e) => setPhoneNumber(e.target.value)} className="w-full p-2 border rounded-md outline-none bg-zinc-700 text-white" - placeholder="1234567890" + placeholder="Enter number" required /> + +
+
+ + setEmail(e.target.value)} + className="w-full p-2 border rounded-md outline-none bg-zinc-700 text-white" + placeholder="Enter name" + /> +
@@ -55,10 +96,22 @@ export default function SignUpPage() { value={password} onChange={(e) => setPassword(e.target.value)} className="w-full p-2 border rounded-md outline-none bg-zinc-700 text-white" - placeholder="••••••••" + placeholder="Enter password" + required + /> +
+
+ + setPin(e.target.value)} + className="w-full p-2 border rounded-md outline-none bg-zinc-700 text-white" + placeholder="Enter pin" required />
+ +
+ + {qrData && ( + +
+ +
+ +
+ + {status || "PENDING"} + + {status === "PAID" && ( + + ₹{amount} Received! 🎉 + + )} +
+ + {status === "PENDING" && ( + + )} +
+ )} +
+ + + ); +} \ No newline at end of file diff --git a/apps/user-app/app/(dashboard)/dashboard/page.tsx b/apps/user-app/app/(dashboard)/dashboard/page.tsx index e2955b5..d8dcbed 100644 --- a/apps/user-app/app/(dashboard)/dashboard/page.tsx +++ b/apps/user-app/app/(dashboard)/dashboard/page.tsx @@ -91,7 +91,7 @@ export default async function DashboardPage() { {/* User Greeting */}
- + {session.user.name?.[0] || "U"}

diff --git a/apps/user-app/app/(dashboard)/layout.tsx b/apps/user-app/app/(dashboard)/layout.tsx index 994ae46..db8ba28 100644 --- a/apps/user-app/app/(dashboard)/layout.tsx +++ b/apps/user-app/app/(dashboard)/layout.tsx @@ -47,7 +47,7 @@ export default function Layout({
- + {session?.user?.name?.[0] || "US"} @@ -56,7 +56,7 @@ export default function Layout({ {session?.user?.name || "Nasir Nadaf"}

- {session?.user?.email || "nasir@gmail.com"} + {session?.user?.number || "+1234567890"}

diff --git a/apps/user-app/app/api/auth/[...nextauth]/route.ts b/apps/user-app/app/api/auth/[...nextauth]/route.ts index 5da81a6..c8d8c83 100644 --- a/apps/user-app/app/api/auth/[...nextauth]/route.ts +++ b/apps/user-app/app/api/auth/[...nextauth]/route.ts @@ -1,6 +1,15 @@ import { authOptions } from "@/app/lib/auth" +import redis from "@/lib/redis" import NextAuth from "next-auth" const handler = NextAuth(authOptions) + +export { handler as GET, handler as POST } -export { handler as GET, handler as POST } \ No newline at end of file +handler.events = { + async signOut({ token }: { token?: { number?: string } }) { + if(token?.number) { + await redis.del(`session:phone:${token.number}`) + } + } +} \ No newline at end of file diff --git a/apps/user-app/app/api/auth/reset/request/route.ts b/apps/user-app/app/api/auth/reset/request/route.ts new file mode 100644 index 0000000..228f166 --- /dev/null +++ b/apps/user-app/app/api/auth/reset/request/route.ts @@ -0,0 +1,50 @@ + +import db from "@repo/db/client"; +import { auth, RecaptchaVerifier, signInWithPhoneNumber } from "@/lib/firebase"; + +export async function POST(req: Request) { + try { + const { phoneNumber } = await req.json(); + + // Find user + const user = await db.user.findUnique({ + where: { number: phoneNumber }, + }); + + if (!user) { + return Response.json({ message: "If the number exists, an OTP was sent." }); + } + + // Generate OTP (fallback for mock) + const otp = String(Math.floor(100000 + Math.random() * 900000)); + const expires = new Date(Date.now() + 10 * 60 * 1000); + + // Save OTP (for mock fallback) + await db.user.update({ + where: { id: user.id }, + data: { resetOtp: otp, resetExpires: expires }, + }); + + // === MOCK SMS (for local dev) === + if (process.env.NODE_ENV === "development") { + console.log(`[MOCK SMS] OTP for ${phoneNumber}: ${otp}`); + return Response.json({ message: "OTP sent (check console)" }); + } + + // === PRODUCTION: Firebase Phone Auth === + try { + const recaptcha = new RecaptchaVerifier(auth, "recaptcha", { size: "invisible" }); + const confirmation = await signInWithPhoneNumber(auth, `+91${phoneNumber}`, recaptcha); + (global as any).confirmationResult = confirmation; // Store for verification + } catch (firebaseError) { + console.error("Firebase OTP failed:", firebaseError); + // Fallback to mock + console.log(`[FALLBACK] OTP for ${phoneNumber}: ${otp}`); + } + + return Response.json({ message: "OTP sent" }); + } catch (e) { + console.error("Reset request error:", e); + return Response.json({ message: "Server error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/apps/user-app/app/api/auth/reset/update/route.ts b/apps/user-app/app/api/auth/reset/update/route.ts new file mode 100644 index 0000000..6fc75b3 --- /dev/null +++ b/apps/user-app/app/api/auth/reset/update/route.ts @@ -0,0 +1,25 @@ + +import db from "@repo/db/client"; +import bcrypt from "bcrypt"; + +export async function POST(req: Request) { + try { + const { userId, newPassword } = await req.json(); + + if (!newPassword || newPassword.length < 6) { + return Response.json({ message: "Password too weak" }, { status: 400 }); + } + + const hashed = await bcrypt.hash(newPassword, 12); + + await db.user.update({ + where: { id: userId }, + data: { password: hashed }, + }); + + return Response.json({ message: "Password updated" }); + } catch (e) { + console.error("Reset update error:", e); + return Response.json({ message: "Server error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/apps/user-app/app/api/auth/reset/verify-firebase/route.ts b/apps/user-app/app/api/auth/reset/verify-firebase/route.ts new file mode 100644 index 0000000..b083b9a --- /dev/null +++ b/apps/user-app/app/api/auth/reset/verify-firebase/route.ts @@ -0,0 +1,54 @@ + +import { getAuth } from "firebase-admin/auth"; +import { initializeApp } from "firebase-admin/app"; +import db from "@repo/db/client"; + +// Extend globalThis to include firebaseAdminApp +declare global { + // eslint-disable-next-line no-var + var firebaseAdminApp: ReturnType | undefined; +} + +// Initialize Firebase Admin once +let adminApp; +if (!global.firebaseAdminApp) { + adminApp = initializeApp(); + global.firebaseAdminApp = adminApp; +} else { + adminApp = global.firebaseAdminApp; +} +const adminAuth = getAuth(adminApp); + +export async function POST(req: Request) { + try { + const { idToken, phone } = await req.json(); + + if (process.env.NODE_ENV === "development" && idToken === "mock-token") { + const user = await db.user.findUnique({ + where: { number: phone.replace("+91", "") }, + }); + if (!user) return Response.json({ error: "User not found" }, { status: 404 }); + return Response.json({ userId: user.id }); + } + + // Verify Firebase token + const decoded = await adminAuth.verifyIdToken(idToken); + if (decoded.phone_number !== phone) { + return Response.json({ error: "Phone mismatch" }, { status: 400 }); + } + + // Find user by phone + const user = await db.user.findUnique({ + where: { number: phone.replace("+91", "") }, + }); + + if (!user) { + return Response.json({ error: "User not found" }, { status: 404 }); + } + + return Response.json({ userId: user.id }); + } catch (e: any) { + console.error("Firebase verify error:", e); + return Response.json({ error: "Invalid token" }, { status: 400 }); + } +} \ No newline at end of file diff --git a/apps/user-app/app/api/auth/signup/route.ts b/apps/user-app/app/api/auth/signup/route.ts new file mode 100644 index 0000000..409f4ff --- /dev/null +++ b/apps/user-app/app/api/auth/signup/route.ts @@ -0,0 +1,38 @@ + +import db from "@repo/db/client"; +import bcrypt from "bcrypt"; + +export async function POST(request: Request){ + + try{ + const { phoneNumber, password, name, email, userPin} = await request.json(); + + if(!phoneNumber || !password){ + return new Response(JSON.stringify({message: "Phone number and password are required"}), {status: 400}); + } + const existingUser = await db.user.findUnique({ + where: {number: phoneNumber} + + }) + if(existingUser){ + return new Response(JSON.stringify({message: "User with this phone number already exists"}), {status: 400}); + } + + const hashedPassword = await bcrypt.hash(password, 10); + const hashedPin = await bcrypt.hash(userPin, 10); + + const newUser = await db.user.create({ + data:{ + name, + number:phoneNumber, + email : email || null, + password: hashedPassword, + userpin : hashedPin, + } + }) + return new Response(JSON.stringify({message: "User created successfully", userId: newUser.id}), {status: 201}); + }catch(error){ + console.error("Error during user signup:", error); + return new Response(JSON.stringify({message: "Internal Server Error"}), {status: 500}); + } +} \ No newline at end of file diff --git a/apps/user-app/app/api/health/route.ts b/apps/user-app/app/api/health/route.ts new file mode 100644 index 0000000..6317528 --- /dev/null +++ b/apps/user-app/app/api/health/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({ status: "ok", timestamp: new Date().toISOString() }); +} diff --git a/apps/user-app/app/api/scan/pay/route.ts b/apps/user-app/app/api/scan/pay/route.ts index 4cbb281..28bae18 100644 --- a/apps/user-app/app/api/scan/pay/route.ts +++ b/apps/user-app/app/api/scan/pay/route.ts @@ -6,9 +6,9 @@ import prisma from "@repo/db/client"; export async function POST(request: Request) { try { const session = await getServerSession(authOptions); - if (!session?.user?.email) { - console.error("No session or email found"); - return NextResponse.json({ error: "Unauthorized: No email in session" }, { status: 401 }); + if (!session?.user?.id) { + console.error("No session or user ID found"); + return NextResponse.json({ error: "Unauthorized: No user in session" }, { status: 401 }); } const { qrId, amount, transactionId } = await request.json(); @@ -18,9 +18,9 @@ export async function POST(request: Request) { return NextResponse.json({ error: "UPI Transaction ID required" }, { status: 400 }); } - const user = await prisma.user.findUnique({ where: { email: session.user.email } }); + const user = await prisma.user.findUnique({ where: { id: Number(session.user.id) } }); if (!user) { - console.error(`User not found for email: ${session.user.email}`); + console.error(`User not found for id: ${session.user.id}`); return NextResponse.json({ error: "User not found" }, { status: 404 }); } diff --git a/apps/user-app/app/api/scan/verify/route.ts b/apps/user-app/app/api/scan/verify/route.ts index 573a45f..8bcac29 100644 --- a/apps/user-app/app/api/scan/verify/route.ts +++ b/apps/user-app/app/api/scan/verify/route.ts @@ -8,9 +8,9 @@ export async function POST(request: Request) { let qrCodeUrl: string | undefined; try { const session = await getServerSession(authOptions); - if (!session?.user?.email) { - console.error("No session or email found"); - return NextResponse.json({ error: "Unauthorized: No email in session" }, { status: 401 }); + if (!session?.user?.id) { + console.error("No session or user ID found"); + return NextResponse.json({ error: "Unauthorized: No user in session" }, { status: 401 }); } const { qrCodeUrl } = await request.json(); diff --git a/apps/user-app/app/api/verify-payment/route.ts b/apps/user-app/app/api/verify-payment/route.ts index bb2b70d..380c17e 100644 --- a/apps/user-app/app/api/verify-payment/route.ts +++ b/apps/user-app/app/api/verify-payment/route.ts @@ -49,8 +49,7 @@ export async function POST(req: NextRequest) { await prisma.onRampTransaction.update({ where: { id: transaction.id }, data: { - status: "Success", - paymentId: razorpay_payment_id, // store Razorpay payment ID + status: "Success" }, }); diff --git a/apps/user-app/app/auth/reset-password/layout.tsx b/apps/user-app/app/auth/reset-password/layout.tsx new file mode 100644 index 0000000..45e038b --- /dev/null +++ b/apps/user-app/app/auth/reset-password/layout.tsx @@ -0,0 +1,9 @@ +import { OTPProvider } from "@/contexts/OTPContext"; + +export default function ResetPasswordLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/apps/user-app/app/auth/reset-password/new/page.tsx b/apps/user-app/app/auth/reset-password/new/page.tsx new file mode 100644 index 0000000..5ee7d04 --- /dev/null +++ b/apps/user-app/app/auth/reset-password/new/page.tsx @@ -0,0 +1,92 @@ +"use client"; +import axios from "axios"; +import { signIn } from "next-auth/react"; +import { useRouter, useSearchParams } from "next/navigation"; +import React, { useState } from "react"; + +const page = () => { + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const router = useRouter(); + const searchParams = useSearchParams(); + const userId = searchParams?.get("userId"); + const phone = searchParams?.get("phone"); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirm) { + setError("Passwords do not match"); + return; + } + setLoading(true); + setError(""); + try { + await axios.post("/api/auth/reset/update", { + userId: Number(userId), + newPassword: password, + }); + const result = await signIn("credentials", { + redirect: false, + phone: phone?.replace("+91", ""), // matches your credentials + password, + }); + if (result?.error) { + setError( + "Password updated, but automatic sign-in failed. Please login manually." + ); + setLoading(false); + return; + } + if (result?.ok) { + router.push("/"); + } else { + setError( + "Password updated, but automatic sign-in failed. Please login manually." + ); + router.push("/auth/signin"); + } + } catch (err: any) { + setError(err.response?.data?.message || "An error occurred"); + } finally { + setLoading(false); + } + }; + return ( +
+
+

Set New Password

+ setPassword(e.target.value)} + placeholder="New password" + className="w-full p-3 bg-zinc-700 rounded-md" + required + /> + setConfirm(e.target.value)} + placeholder="Confirm password" + className="w-full p-3 bg-zinc-700 rounded-md" + required + /> + {error &&

{error}

} + +
+
+ ); +}; + +export default page; diff --git a/apps/user-app/app/auth/reset-password/otp/page.tsx b/apps/user-app/app/auth/reset-password/otp/page.tsx new file mode 100644 index 0000000..f065cda --- /dev/null +++ b/apps/user-app/app/auth/reset-password/otp/page.tsx @@ -0,0 +1,154 @@ +"use client"; +import { useState, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import axios from "axios"; +import { useOtp } from "@/contexts/OTPContext"; +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from "@/components/ui/input-otp"; + +const MAX_ATTEMPTS = 3; +const BLOCK_DURATION_MS = 60_000; + +export default function OTPPage() { + const [otp, setOtp] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [attempts, setAttempts] = useState(0); + const [blockedUntil, setBlockedUntil] = useState(null); + + const router = useRouter(); + const searchParams = useSearchParams(); + const phone = searchParams?.get("phone"); + const { confirmationResult } = useOtp(); + + const storageKey = `otp_lock_${phone}`; + + useEffect(() => { + const saved = localStorage.getItem(storageKey); + if (saved) { + const data = JSON.parse(saved); + setAttempts(data.attempts); + if (data.blockedUntil && data.blockedUntil > Date.now()) { + setBlockedUntil(data.blockedUntil); + } else { + localStorage.removeItem(storageKey); + } + } + }, [phone, storageKey]); + + const timeLeft = blockedUntil + ? Math.max(0, Math.ceil((blockedUntil - Date.now()) / 1000)) + : 0; + + const saveLock = (newAttempts: number, newBlockedUntil: number | null) => { + localStorage.setItem( + storageKey, + JSON.stringify({ attempts: newAttempts, blockedUntil: newBlockedUntil }) + ); + }; + + const handleVerify = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + + if (blockedUntil && blockedUntil > Date.now()) { + setError(`Too many attempts. Try again in ${timeLeft}s.`); + setLoading(false); + return; + } + + if (!confirmationResult) { + setError("Session expired. Request OTP again."); + setLoading(false); + return; + } + + try { + const result = await confirmationResult.confirm(otp); + const idToken = await result.user.getIdToken(); + + const res = await axios.post("/api/auth/reset/verify-firebase", { + idToken, + phone: `+91${phone}`, + }); + + localStorage.removeItem(storageKey); + router.push( + `/auth/reset-password/new?userId=${res.data.userId}&phone=${phone}` + ); + } catch (err: any) { + const newAttempts = attempts + 1; + setAttempts(newAttempts); + + if (newAttempts >= MAX_ATTEMPTS) { + const blockUntil = Date.now() + BLOCK_DURATION_MS; + setBlockedUntil(blockUntil); + saveLock(newAttempts, blockUntil); + setError(`Too many failed attempts. Try again in 60s.`); + } else { + saveLock(newAttempts, null); + setError(`Invalid OTP. ${MAX_ATTEMPTS - newAttempts} attempt(s) left.`); + } + } finally { + setLoading(false); + } + }; + + return ( +
+
+ +

Enter OTP

+

Sent to +91{phone}

+ + + + + + + + + + + + + + + + + + {error &&

{error}

} + + {blockedUntil && timeLeft > 0 && ( +

+ Retry in {timeLeft}s +

+ )} + + + + +
+
+ ); +} diff --git a/apps/user-app/app/auth/reset-password/page.tsx b/apps/user-app/app/auth/reset-password/page.tsx new file mode 100644 index 0000000..282ae63 --- /dev/null +++ b/apps/user-app/app/auth/reset-password/page.tsx @@ -0,0 +1,87 @@ +"use client"; +import { useState } from "react"; +import { auth, RecaptchaVerifier, signInWithPhoneNumber } from "@/lib/firebase"; +import { useRouter } from "next/navigation"; +import { useOtp } from "@/contexts/OTPContext"; + +declare global { + interface Window { + confirmationResult: any; + } +} + +export default function ResetPassword() { + const [phone, setPhone] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const router = useRouter(); + const { setConfirmationResult } = useOtp(); + + const handleSendOTP = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + localStorage.removeItem(`otpAttempts_${phone}`); + if (process.env.NODE_ENV === "development") { + const mockOTP = String(Math.floor(100000 + Math.random() * 900000)); + console.log(`[MOCK OTP] For +91${phone}: ${mockOTP}`); + setConfirmationResult({ + confirm: async (otp: string) => { + if (otp === mockOTP) { + return { user: { getIdToken: async () => "mock-token" } }; + } + throw new Error("Invalid OTP"); + }, + } as any); + + router.push(`/auth/reset-password/otp?phone=${phone}`); + return; + } + + + const recaptcha = new RecaptchaVerifier(auth, "recaptcha-container", { + size: "invisible", + }); + + const confirmation = await signInWithPhoneNumber(auth, `+91${phone}`, recaptcha); + window.confirmationResult = confirmation; + + router.push(`/auth/reset-password/otp?phone=${phone}`); + } catch (err: any) { + setError(err.message || "Failed to send OTP"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Reset Password

+
+ +91 + setPhone(e.target.value.replace(/\D/g, ""))} + placeholder="Enter phone number" + className="flex-1 p-3 bg-zinc-700 rounded-r-md outline-none" + maxLength={10} + required + /> +
+ {error &&

{error}

} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/user-app/app/auth/signin/page.tsx b/apps/user-app/app/auth/signin/page.tsx index 3a3e3b0..c7db101 100644 --- a/apps/user-app/app/auth/signin/page.tsx +++ b/apps/user-app/app/auth/signin/page.tsx @@ -7,19 +7,44 @@ import { CardFooter, CardHeader, CardTitle, -} from "../../../components/ui/card"; +} from "@/components/ui/card"; +import { useSearchParams } from "next/navigation"; export default function SignInPage() { const [phone, setPhone] = useState(""); const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const searchParams = useSearchParams(); + const callbackUrl = searchParams?.get("callbackUrl") || "/"; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await signIn("credentials", { + setError(""); + setLoading(true); + + const result = await signIn("credentials", { phone, password, - callbackUrl: "/", + callbackUrl, + redirect: false, }); + + if (result?.error) { + // === RATE LIMIT & ATTEMPTS === + if (result.error.includes("Too many login attempts")) { + setError("Too many attempts. Try again in 1 minute."); + } else if (result.error.includes("attempts left")) { + setError(result.error); // e.g., "Invalid credentials. 3 attempts left." + } else { + setError("Invalid phone number or password"); + } + } else if (result?.ok) { + window.location.href = callbackUrl; + } + + setLoading(false); }; return ( @@ -28,44 +53,68 @@ export default function SignInPage() { Sign In +
- + setPhone(e.target.value)} - className="w-full p-2 border rounded-md outline-none bg-zinc-700 " + onChange={(e) => setPhone(e.target.value.replace(/\D/g, "").slice(0, 10))} + className="w-full p-2 border rounded-md outline-none bg-zinc-700 text-white placeholder-gray-400" placeholder="Enter Number" + maxLength={10} required + disabled={loading} />
+
+ + {/* ERROR MESSAGE */} + {error && ( +
+ {error} +
+ )} +
- - Don’t have an account? Sign Up + + +

+ Don’t have an account?{" "} + + Sign Up + +

); -} +} \ No newline at end of file diff --git a/apps/user-app/app/auth/signup/page.tsx b/apps/user-app/app/auth/signup/page.tsx index c8d29e7..c575bdd 100644 --- a/apps/user-app/app/auth/signup/page.tsx +++ b/apps/user-app/app/auth/signup/page.tsx @@ -3,48 +3,84 @@ import { useState } from "react"; import { Card, CardContent, + CardDescription, CardFooter, CardHeader, CardTitle, } from "../../../components/ui/card"; import { signIn } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import axios from "axios" export default function SignUpPage() { const [phoneNumber, setPhoneNumber] = useState(""); const [password, setPassword] = useState(""); const [name , setName] = useState(""); const [email , setEmail] = useState(""); - const [pin , setPin] = useState(""); - const [img , setImg] = useState(""); + const [userPin , setPin] = useState(""); + const [image , setImg] = useState(""); + const [error , setError] = useState("") + const router = useRouter() const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - await signIn("credentials", { + e.preventDefault(); + setError(""); + + try { + const signupResponse = await axios.post("/api/auth/signup", { + name, phoneNumber, password, - name, email, - pin, - img, - callbackUrl: "/", - }) + userPin, + }); - }; + console.log("Signup success:", signupResponse.data); + + + const signInResult = await signIn<"credentials">("credentials", { + redirect: false, + phone: phoneNumber, + password, + }); + + if (signInResult?.error) { + setError("Signup succeeded, but login failed. Try signing in manually."); + router.push("/auth/signin"); + return; + } else { + router.push("/home"); + } + } catch (err: any) { + console.error("Signup error:", err); + const msg = err.response?.data?.message || err.message || "Something went wrong."; + setError(msg); + } +}; return (
- Sign Up + Create Account + {error &&

{error}

}
setImg(e.target.value)} + onChange={(e) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setImg(reader.result as string); + }; + reader.readAsDataURL(file); + } + }} className="p-2 border rounded-md hover:cursor-pointer" />
@@ -81,11 +117,11 @@ export default function SignUpPage() { Email setEmail(e.target.value)} className="w-full p-2 border rounded-md outline-none bg-zinc-700 text-white" - placeholder="Enter name" + placeholder="Enter email" />
@@ -104,7 +140,7 @@ export default function SignUpPage() { setPin(e.target.value)} className="w-full p-2 border rounded-md outline-none bg-zinc-700 text-white" placeholder="Enter pin" @@ -116,11 +152,13 @@ export default function SignUpPage() { type="submit" className="w-full bg-zinc-500 text-white py-2 rounded-md hover:bg-zinc-600 transition" > - Sign Up + Create Account + + Already have an account?{" "} 5) { + throw new Error( + "Too many login attempts. Please try again in 1 minute." + ); + } const user = await db.user.findUnique({ where: { number: phone }, @@ -58,9 +77,24 @@ export const authOptions: AuthOptions = { if (!user) return null; const ok = await bcrypt.compare(password, user.password); - if (!ok) return null; + if (!ok) { + const remaining = 5 - attempts; + if (remaining > 0) { + throw new Error( + `Invalid credentials. ${remaining} attempt(s) left.` + ); + } else { + throw new Error("Too many login attempts. Try again in 1 minute."); + } + } + await redis.del(rateKey); // reset on successful login - const sessionToken = await createNewSessionToken(user.id); + const sessionToken = uuid(); + await db.user.update({ + where: { id: user.id }, + data: { sessionToken }, + }); + await redis.setex(`session:phone:${phone}`, SESSION_TTL, sessionToken); return { id: user.id.toString(), @@ -85,15 +119,26 @@ export const authOptions: AuthOptions = { token.sessionToken = user.sessionToken; } - if (token.number && token.sessionToken) { + if (!token.number) return token; + + const cacheKey = `session:phone:${token.number}`; + const cachedToken = await redis.get(cacheKey); + + if (cachedToken && cachedToken !== token.sessionToken) { + throw new Error("SESSION_EXPIRED_ANOTHER_DEVICE"); + } + + if (!cachedToken) { const dbUser = await db.user.findUnique({ where: { number: token.number }, select: { sessionToken: true }, }); - if (!dbUser || dbUser.sessionToken !== token.sessionToken) { - throw new Error("SESSION_EXPIRED_ANOTHER_DEVICE"); + if (!dbUser) { + throw new Error("USER_NOT_FOUND"); } + // repopulate cache + await redis.setex(cacheKey, SESSION_TTL, dbUser.sessionToken!); } return token; @@ -110,4 +155,4 @@ export const authOptions: AuthOptions = { pages: { signIn: "/auth/signin", }, -}; \ No newline at end of file +}; diff --git a/apps/user-app/app/not-found.tsx b/apps/user-app/app/not-found.tsx new file mode 100644 index 0000000..650db6b --- /dev/null +++ b/apps/user-app/app/not-found.tsx @@ -0,0 +1,12 @@ +'use client' + +export default function NotFound() { + return ( +
+
+

404

+

Page Not Found

+
+
+ ); +} diff --git a/apps/user-app/components/ui/floating-navbar.tsx b/apps/user-app/components/ui/floating-navbar.tsx index e65d602..45dbd08 100644 --- a/apps/user-app/components/ui/floating-navbar.tsx +++ b/apps/user-app/components/ui/floating-navbar.tsx @@ -71,7 +71,7 @@ export const FloatingNav = ({ - + {session?.user?.name?.[0] || "US"} diff --git a/apps/user-app/components/ui/input-otp.tsx b/apps/user-app/components/ui/input-otp.tsx new file mode 100644 index 0000000..0cd74c0 --- /dev/null +++ b/apps/user-app/components/ui/input-otp.tsx @@ -0,0 +1,74 @@ +"use client" + +import * as React from "react" +import { OTPInput, OTPInputContext } from "input-otp" +import { Minus } from "lucide-react" + +import { cn } from "@/lib/utils" + +const InputOTP = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)) +InputOTP.displayName = "InputOTP" + +const InputOTPGroup = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ className, ...props }, ref) => ( +
+)) +InputOTPGroup.displayName = "InputOTPGroup" + +const InputOTPSlot = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = React.useContext(OTPInputContext) + const slot = inputOTPContext.slots[index] + const char = slot?.char + const hasFakeCaret = slot?.hasFakeCaret + const isActive = slot?.isActive + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ) +}) +InputOTPSlot.displayName = "InputOTPSlot" + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ ...props }, ref) => ( +
+ +
+)) +InputOTPSeparator.displayName = "InputOTPSeparator" + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } diff --git a/apps/user-app/components/ui/toaster.tsx b/apps/user-app/components/ui/toaster.tsx index 171beb4..e16708d 100644 --- a/apps/user-app/components/ui/toaster.tsx +++ b/apps/user-app/components/ui/toaster.tsx @@ -1,14 +1,8 @@ "use client" import { useToast } from "@/hooks/use-toast" -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast" +import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "../../../../packages/ui/src/toast" + export function Toaster() { const { toasts } = useToast() diff --git a/apps/user-app/contexts/OTPContext.tsx b/apps/user-app/contexts/OTPContext.tsx new file mode 100644 index 0000000..79a44ce --- /dev/null +++ b/apps/user-app/contexts/OTPContext.tsx @@ -0,0 +1,29 @@ +"use client"; +import { createContext, ReactNode, useContext, useState } from "react"; + +type ConfirmationResult ={ + confirm: (code: string) => Promise; +} + +type OTPContextType = { + confirmationResult: ConfirmationResult | null; + setConfirmationResult: (cr: ConfirmationResult | null) => void; +} + +const OTPContext = createContext(undefined); + +export const OTPProvider = ({ children } :{ children: ReactNode }) => { + const [confirmationResult, setConfirmationResult] = useState(null) + return ( + + {children} + + ) +} +export const useOtp =() => { + const context = useContext(OTPContext); + if (!context) { + throw new Error("useOtp must be used within an OTPProvider"); + } + return context; +} \ No newline at end of file diff --git a/apps/user-app/hooks/use-toast.ts b/apps/user-app/hooks/use-toast.ts index 02e111d..e46b0fb 100644 --- a/apps/user-app/hooks/use-toast.ts +++ b/apps/user-app/hooks/use-toast.ts @@ -2,11 +2,8 @@ // Inspired by react-hot-toast library import * as React from "react" +import { ToastActionElement, ToastProps } from "../../../packages/ui/src/toast" -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" const TOAST_LIMIT = 1 const TOAST_REMOVE_DELAY = 1000000 diff --git a/apps/user-app/lib/firebase.ts b/apps/user-app/lib/firebase.ts new file mode 100644 index 0000000..6a3efd7 --- /dev/null +++ b/apps/user-app/lib/firebase.ts @@ -0,0 +1,18 @@ +// lib/firebase.ts +import { getApps, initializeApp } from "firebase/app"; +import { getAuth, RecaptchaVerifier, signInWithPhoneNumber } from "firebase/auth"; + +const firebaseConfig = { + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: "calx-d05b2.firebasestorage.app", + messagingSenderId: "675190342157", + appId: "1:675190342157:web:b69e0beb329262039f0b44", + measurementId: "G-DHJ7EF75QL" +}; + +const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; +const auth = getAuth(app); + +export { auth, RecaptchaVerifier, signInWithPhoneNumber }; \ No newline at end of file diff --git a/apps/user-app/lib/redis.ts b/apps/user-app/lib/redis.ts new file mode 100644 index 0000000..16ce8fc --- /dev/null +++ b/apps/user-app/lib/redis.ts @@ -0,0 +1,51 @@ +import Redis from "ioredis"; + +let redis: Redis | null = null; + +function createRedis(): Redis { + if (redis) return redis; + + redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379", { + lazyConnect: true, + maxRetriesPerRequest: 1, + retryStrategy: () => null, // Don't retry + enableOfflineQueue: false, + enableReadyCheck: false, + }); + + // Suppress all Redis errors during build/runtime + redis.on("error", () => { + // Silently handle errors + }); + + return redis; +} + +// Proxy that wraps all Redis operations with error handling +const redisProxy = new Proxy({} as Redis, { + get(_target, prop: string) { + const client = createRedis(); + const original = (client as any)[prop]; + + if (typeof original === "function") { + return async (...args: any[]) => { + try { + // Only connect if not already connected + if (redis && redis.status === "wait") { + await redis.connect().catch(() => {}); + } + return await original.apply(client, args); + } catch (error: any) { + // Return safe defaults for build-time failures + if (prop === "get") return null; + if (prop === "incr") return 0; + // For other methods, return undefined (no-op) + return undefined; + } + }; + } + return original; + }, +}); + +export default redisProxy; \ No newline at end of file diff --git a/apps/user-app/next-env.d.ts b/apps/user-app/next-env.d.ts index 725dd6f..36a4fe4 100644 --- a/apps/user-app/next-env.d.ts +++ b/apps/user-app/next-env.d.ts @@ -1,6 +1,7 @@ /// /// /// +/// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/user-app/provider.tsx b/apps/user-app/provider.tsx index 7835707..9176ecc 100644 --- a/apps/user-app/provider.tsx +++ b/apps/user-app/provider.tsx @@ -1,11 +1,25 @@ "use client" import { RecoilRoot } from "recoil"; import { SessionProvider } from "next-auth/react"; +import { useEffect, useState } from "react"; export const Providers = ({children}: {children: React.ReactNode}) => { - return - - {children} - - + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + // During build/SSR, render children without providers + if (!mounted) { + return <>{children}; + } + + return ( + + + {children} + + + ); } \ No newline at end of file diff --git a/apps/user-app/tsconfig.json b/apps/user-app/tsconfig.json index 4881d60..8cf8f9c 100644 --- a/apps/user-app/tsconfig.json +++ b/apps/user-app/tsconfig.json @@ -18,8 +18,27 @@ "next.config.js", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts" -, "components/AddMoneyCard.tsx", "components/ui/FeatureSection.tsx", "../../packages/ui/src/animated-testimonials.tsx", "../../packages/ui/src/3d-marquee.tsx", "../../packages/ui/src/avatar.tsx", "../../packages/ui/src/background-boxes.tsx", "../../packages/ui/src/tooltip.tsx", "components/ui/toaster.tsx", "../../packages/ui/src/toast.tsx", "../../packages/ui/src/text-hover-effect.tsx", "../../packages/ui/src/tabs.tsx", "../../packages/ui/src/table.tsx", "components/ui/switch.tsx", "../../packages/ui/src/skeleton.tsx", "../../packages/ui/src/sidebar.tsx", "../../packages/ui/src/separator.tsx", "../../packages/ui/src/label.tsx", "components/ui/input.tsx", "../../packages/ui/src/layout-text-flip.tsx" ], + ".next/types/**/*.ts", + "components/AddMoneyCard.tsx", + "components/ui/FeatureSection.tsx", + "../../packages/ui/src/animated-testimonials.tsx", + "../../packages/ui/src/3d-marquee.tsx", + "../../packages/ui/src/avatar.tsx", + "../../packages/ui/src/background-boxes.tsx", + "../../packages/ui/src/tooltip.tsx", + "components/ui/toaster.tsx", + "../../packages/ui/src/toast.tsx", + "../../packages/ui/src/text-hover-effect.tsx", + "../../packages/ui/src/tabs.tsx", + "../../packages/ui/src/table.tsx", + "components/ui/switch.tsx", + "../../packages/ui/src/skeleton.tsx", + "../../packages/ui/src/sidebar.tsx", + "../../packages/ui/src/separator.tsx", + "../../packages/ui/src/label.tsx", + "components/ui/input.tsx", + "../../packages/ui/src/layout-text-flip.tsx" + ], "exclude": [ "node_modules" ] diff --git a/packages/db/.gitignore b/packages/db/.gitignore index 385ff32..11ddd8d 100644 --- a/packages/db/.gitignore +++ b/packages/db/.gitignore @@ -1,4 +1,3 @@ node_modules # Keep environment variables out of version control .env -migrations diff --git a/packages/db/prisma/migrations/20251031084348_second_init/migration.sql b/packages/db/prisma/migrations/20251031084348_second_init/migration.sql new file mode 100644 index 0000000..8e802c0 --- /dev/null +++ b/packages/db/prisma/migrations/20251031084348_second_init/migration.sql @@ -0,0 +1,182 @@ +-- CreateEnum +CREATE TYPE "BillType" AS ENUM ('ELECTRICITY', 'WATER', 'GAS', 'PHONE_RECHARGE', 'DTH'); + +-- CreateEnum +CREATE TYPE "P2PRequestStatus" AS ENUM ('PENDING', 'SETTLED', 'EXPIRED', 'CANCELLED'); + +-- CreateEnum +CREATE TYPE "AuthType" AS ENUM ('Google', 'Github'); + +-- CreateEnum +CREATE TYPE "OnRampStatus" AS ENUM ('Success', 'Failure', 'Processing'); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT, + "name" TEXT, + "number" TEXT NOT NULL, + "password" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userpin" TEXT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "p2pTransfer" ( + "id" SERIAL NOT NULL, + "amount" INTEGER NOT NULL, + "timestamp" TIMESTAMP(3) NOT NULL, + "fromUserId" INTEGER NOT NULL, + "toUserId" INTEGER NOT NULL, + + CONSTRAINT "p2pTransfer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Merchant" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + "upiId" TEXT, + "auth_type" "AuthType" NOT NULL, + + CONSTRAINT "Merchant_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MerchantPayment" ( + "id" SERIAL NOT NULL, + "merchantId" INTEGER NOT NULL, + "qrId" TEXT NOT NULL, + "amount" INTEGER NOT NULL, + "status" TEXT NOT NULL, + "userId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "transactionId" TEXT, + + CONSTRAINT "MerchantPayment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OnRampTransaction" ( + "id" SERIAL NOT NULL, + "status" "OnRampStatus" NOT NULL, + "token" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "amount" INTEGER NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "transactionId" TEXT, + "userId" INTEGER NOT NULL, + + CONSTRAINT "OnRampTransaction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "p2p_requests" ( + "id" SERIAL NOT NULL, + "senderId" INTEGER NOT NULL, + "receiverId" INTEGER, + "receiverNumber" TEXT NOT NULL, + "amount" INTEGER NOT NULL, + "message" TEXT, + "status" "P2PRequestStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "settledAt" TIMESTAMP(3), + + CONSTRAINT "p2p_requests_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "bill_schedules" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "merchantId" INTEGER, + "billType" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "accountNo" TEXT NOT NULL, + "amount" INTEGER NOT NULL, + "dueDate" TIMESTAMP(3) NOT NULL, + "nextPayment" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "paymentMethod" TEXT NOT NULL DEFAULT 'UPI', + "status" TEXT NOT NULL DEFAULT 'PENDING', + "token" TEXT, + + CONSTRAINT "bill_schedules_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Balance" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "amount" INTEGER NOT NULL, + "locked" INTEGER NOT NULL, + + CONSTRAINT "Balance_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_number_key" ON "User"("number"); + +-- CreateIndex +CREATE UNIQUE INDEX "Merchant_email_key" ON "Merchant"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Merchant_upiId_key" ON "Merchant"("upiId"); + +-- CreateIndex +CREATE UNIQUE INDEX "MerchantPayment_qrId_key" ON "MerchantPayment"("qrId"); + +-- CreateIndex +CREATE UNIQUE INDEX "MerchantPayment_transactionId_key" ON "MerchantPayment"("transactionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "OnRampTransaction_token_key" ON "OnRampTransaction"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "OnRampTransaction_transactionId_key" ON "OnRampTransaction"("transactionId"); + +-- CreateIndex +CREATE INDEX "OnRampTransaction_userId_startTime_idx" ON "OnRampTransaction"("userId", "startTime"); + +-- CreateIndex +CREATE UNIQUE INDEX "bill_schedules_token_key" ON "bill_schedules"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "Balance_userId_key" ON "Balance"("userId"); + +-- AddForeignKey +ALTER TABLE "p2pTransfer" ADD CONSTRAINT "p2pTransfer_fromUserId_fkey" FOREIGN KEY ("fromUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "p2pTransfer" ADD CONSTRAINT "p2pTransfer_toUserId_fkey" FOREIGN KEY ("toUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MerchantPayment" ADD CONSTRAINT "MerchantPayment_merchantId_fkey" FOREIGN KEY ("merchantId") REFERENCES "Merchant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MerchantPayment" ADD CONSTRAINT "MerchantPayment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OnRampTransaction" ADD CONSTRAINT "OnRampTransaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "p2p_requests" ADD CONSTRAINT "p2p_requests_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "p2p_requests" ADD CONSTRAINT "p2p_requests_receiverId_fkey" FOREIGN KEY ("receiverId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bill_schedules" ADD CONSTRAINT "bill_schedules_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "bill_schedules" ADD CONSTRAINT "bill_schedules_merchantId_fkey" FOREIGN KEY ("merchantId") REFERENCES "Merchant"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Balance" ADD CONSTRAINT "Balance_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/20251031090240_updated/migration.sql b/packages/db/prisma/migrations/20251031090240_updated/migration.sql new file mode 100644 index 0000000..d54aa00 --- /dev/null +++ b/packages/db/prisma/migrations/20251031090240_updated/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "sessionToken" DROP NOT NULL; diff --git a/packages/db/prisma/migrations/20251031094706_added_image/migration.sql b/packages/db/prisma/migrations/20251031094706_added_image/migration.sql new file mode 100644 index 0000000..3ff667e --- /dev/null +++ b/packages/db/prisma/migrations/20251031094706_added_image/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "image" TEXT; diff --git a/packages/db/prisma/migrations/20251101033622_otp_options/migration.sql b/packages/db/prisma/migrations/20251101033622_otp_options/migration.sql new file mode 100644 index 0000000..9977c30 --- /dev/null +++ b/packages/db/prisma/migrations/20251101033622_otp_options/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "resetExpires" TIMESTAMP(3), +ADD COLUMN "resetOtp" TEXT; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 12b82e8..7a47b5a 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -16,6 +16,8 @@ model User { sessionToken String? image String? userpin String + resetOtp String? + resetExpires DateTime? OnRampTransaction OnRampTransaction[] Balance Balance[] sentTransfers p2pTransfer[] @relation(name: "FromUserRelation") diff --git a/packages/ui/src/sidebar.tsx b/packages/ui/src/sidebar.tsx index 0c4b5e3..8ce368d 100644 --- a/packages/ui/src/sidebar.tsx +++ b/packages/ui/src/sidebar.tsx @@ -202,7 +202,7 @@ export const SignupBtn = () => { const handleLogout = async () => { try { await signOut({ redirect: false }); - router.push("/auth/signup"); + router.push("/auth/signin"); } catch (error) { console.error("Logout failed:", error); } From 866f88cd15113f47c9610f02761987b3aa8bf80b Mon Sep 17 00:00:00 2001 From: Nasir Nadaf <109416738+VisibleNasir@users.noreply.github.com> Date: Sun, 2 Nov 2025 20:12:52 +0530 Subject: [PATCH 3/3] refactor(TextInput): update layout and label styling - Changed the container to a flex column layout for better alignment. - Updated label font size from 'text-sm' to 'text-md' for improved readability. --- apps/merchant-app/@/components/ui/badge.tsx | 36 + apps/merchant-app/app/api/bills/route.ts | 73 +- apps/merchant-app/app/bills/page.tsx | 166 ++-- apps/merchant-app/app/globals.css | 67 +- apps/merchant-app/app/home/page.tsx | 10 +- apps/merchant-app/app/qr/page.tsx | 113 +-- apps/merchant-app/components.json | 24 + apps/merchant-app/components/atoms/Button.tsx | 8 +- .../components/molecules/QRPaymentHero.tsx | 226 +++-- apps/merchant-app/components/ui/lamp.tsx | 104 +++ apps/merchant-app/package.json | 16 + apps/merchant-app/tailwind.config.js | 55 +- package-lock.json | 841 +++++++++++++++++- packages/ui/src/TextInput.tsx | 4 +- 14 files changed, 1371 insertions(+), 372 deletions(-) create mode 100644 apps/merchant-app/@/components/ui/badge.tsx create mode 100644 apps/merchant-app/components.json create mode 100644 apps/merchant-app/components/ui/lamp.tsx diff --git a/apps/merchant-app/@/components/ui/badge.tsx b/apps/merchant-app/@/components/ui/badge.tsx new file mode 100644 index 0000000..00a860b --- /dev/null +++ b/apps/merchant-app/@/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "../../../lib/utils" + + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/apps/merchant-app/app/api/bills/route.ts b/apps/merchant-app/app/api/bills/route.ts index add3a69..a80e3e1 100644 --- a/apps/merchant-app/app/api/bills/route.ts +++ b/apps/merchant-app/app/api/bills/route.ts @@ -1,47 +1,44 @@ + import { NextResponse } from "next/server"; import { PrismaClient } from "@repo/db/client"; -const prisma = new PrismaClient(); - -export async function POST(request: Request) { - try{ - const { searchParams } = new URL(request.url); - const merchantId = searchParams.get("merchantId"); - if(!merchantId){ - return NextResponse.json({ message: "Merchant ID is required" }, { status: 400 }); - } - const merchant = await prisma.merchant.findUnique({ - where: { id: parseInt(merchantId) } - }); - - if(!merchant){ - return NextResponse.json({ message: "Merchant not found" }, { status: 404 }); - } - - const bills = await prisma.billSchedule.findMany({ - where: { merchantId: parseInt(merchantId) }, - orderBy: { dueDate: "asc" }, - }); - return NextResponse.json(bills, { status: 200 }); - } catch (error) { - console.error("Error fetching bills:", error); - return NextResponse.json({ error: "Failed to fetch bills" }, { status: 500 }); +const prisma = new PrismaClient(); + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const merchantId = searchParams.get("merchantId"); + if (!merchantId) { + return NextResponse.json({ message: "Merchant ID required" }, { status: 400 }); } + + const bills = await prisma.billSchedule.findMany({ + where: { merchantId: parseInt(merchantId) }, + orderBy: { dueDate: "asc" }, + }); + + return NextResponse.json(bills, { status: 200 }); + } catch (error) { + console.error("Error:", error); + return NextResponse.json({ error: "Failed" }, { status: 500 }); + } } export async function PATCH(request: Request) { - try{ - const { billId, status } = await request.json(); - if(!billId || !["PAID", "OVERDUE", "CANCELLED"].includes(status)){ - return NextResponse.json({ message: "Invalid input" }, { status: 400 }); - } - const bill = await prisma.billSchedule.update({ - where: { id: parseInt(billId) }, - data: { status } - }); - return NextResponse.json({ message: "Bill status updated", bill }, { status: 200 }); - } catch (error) { - console.error("Error updating bill status:", error); - return NextResponse.json({ error: "Failed to update bill status" }, { status: 500 }); + try { + const { billId, status } = await request.json(); + if (!billId || !["PAID", "OVERDUE", "CANCELLED"].includes(status)) { + return NextResponse.json({ message: "Invalid" }, { status: 400 }); } + + const bill = await prisma.billSchedule.update({ + where: { id: parseInt(billId) }, + data: { status }, + }); + + return NextResponse.json({ message: "Updated", bill }, { status: 200 }); + } catch (error) { + console.error("Error:", error); + return NextResponse.json({ error: "Failed" }, { status: 500 }); + } } \ No newline at end of file diff --git a/apps/merchant-app/app/bills/page.tsx b/apps/merchant-app/app/bills/page.tsx index d51c6df..4d072f0 100644 --- a/apps/merchant-app/app/bills/page.tsx +++ b/apps/merchant-app/app/bills/page.tsx @@ -1,6 +1,8 @@ "use client"; import { useEffect, useState } from "react"; import { Button } from "@repo/ui/button"; +import { motion } from "framer-motion"; +import { format } from "date-fns"; type BillSchedule = { id: number; @@ -19,103 +21,95 @@ type BillSchedule = { export default function MerchantBillsPage() { const [bills, setBills] = useState([]); - const merchantId = 1; // Replace with authenticated merchant ID + const merchantId = 1; useEffect(() => { - const fetchBills = async () => { - try { - const response = await fetch(`/api/bills?merchantId=${merchantId}`); - const data = await response.json(); - setBills(data); - } catch (error) { - console.error("Error fetching merchant bills:", error); - } - }; - fetchBills(); + fetch(`/api/bills?merchantId=${merchantId}`) + .then((r) => r.json()) + .then(setBills) + .catch(() => setBills([])); }, []); - const handleStatusUpdate = async (billId: number, status: "PAID" | "OVERDUE") => { - try { - const response = await fetch("/api/bills", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ billId, status }), - }); - if (response.ok) { - setBills(bills.map(bill => bill.id === billId ? { ...bill, status } : bill)); - } - } catch (error) { - console.error("Error updating bill status:", error); - } + const updateStatus = async (billId: number, status: "PAID" | "OVERDUE") => { + await fetch("/api/bills", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ billId, status }), + }); + setBills(bills.map((b) => (b.id === billId ? { ...b, status } : b))); }; return ( -
+
-

Merchant Bill Management

-
-

All Bills ({bills.length})

-
- {bills.map((bill) => { - const dueDate = new Date(bill.dueDate); - const isOverdue = dueDate < new Date() && bill.status !== "PAID"; + {/* HEADER */} + +

+ Merchant Bill Dashboard +

+

Total Bills: {bills.length}

+
- return ( -
-

{bill.billType}

-

Provider: {bill.provider}

-

User ID: {bill.userId}

-

- ₹{(bill.amount / 100).toFixed(2)} -

-

- Due: {dueDate.toLocaleDateString("en-IN", { - day: "numeric", - month: "long", - year: "numeric", - })} -

-

- Status: {bill.status} -

-

Payment Method: {bill.paymentMethod}

- {bill.nextPayment && ( -

- Next Payment: {new Date(bill.nextPayment).toLocaleDateString("en-IN", { - day: "numeric", - month: "long", - year: "numeric", - })} -

- )} - {bill.status === "PENDING" && ( -
- - -
- )} + {/* BILLS GRID */} +
+ {bills.map((bill) => { + const due = new Date(bill.dueDate); + const overdue = due < new Date() && bill.status !== "PAID"; + + return ( + +
+

{bill.billType}

+ + {bill.status} +
- ); - })} -
+ +

Provider: {bill.provider}

+

+ ₹{(bill.amount / 100).toFixed(2)} +

+

+ Due: {format(due, "dd MMM yyyy")} +

+ + {bill.status === "PENDING" && ( +
+ + +
+ )} + + ); + })}
); -} \ No newline at end of file +} diff --git a/apps/merchant-app/app/globals.css b/apps/merchant-app/app/globals.css index bd6213e..7dbbfc8 100644 --- a/apps/merchant-app/app/globals.css +++ b/apps/merchant-app/app/globals.css @@ -1,3 +1,68 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 224 71.4% 4.1%; + --card: 0 0% 100%; + --card-foreground: 224 71.4% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 224 71.4% 4.1%; + --primary: 220.9 39.3% 11%; + --primary-foreground: 210 20% 98%; + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + --muted: 220 14.3% 95.9%; + --muted-foreground: 220 8.9% 46.1%; + --accent: 220 14.3% 95.9%; + --accent-foreground: 220.9 39.3% 11%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 20% 98%; + --border: 220 13% 91%; + --input: 220 13% 91%; + --ring: 224 71.4% 4.1%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem + } + .dark { + --background: 224 71.4% 4.1%; + --foreground: 210 20% 98%; + --card: 224 71.4% 4.1%; + --card-foreground: 210 20% 98%; + --popover: 224 71.4% 4.1%; + --popover-foreground: 210 20% 98%; + --primary: 210 20% 98%; + --primary-foreground: 220.9 39.3% 11%; + --secondary: 215 27.9% 16.9%; + --secondary-foreground: 210 20% 98%; + --muted: 215 27.9% 16.9%; + --muted-foreground: 217.9 10.6% 64.9%; + --accent: 215 27.9% 16.9%; + --accent-foreground: 210 20% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 20% 98%; + --border: 215 27.9% 16.9%; + --input: 215 27.9% 16.9%; + --ring: 216 12.2% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55% + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/apps/merchant-app/app/home/page.tsx b/apps/merchant-app/app/home/page.tsx index e4ca929..d60e3ca 100644 --- a/apps/merchant-app/app/home/page.tsx +++ b/apps/merchant-app/app/home/page.tsx @@ -22,7 +22,7 @@ const page = () => { const navItems = [ { name: "Home", link: "/" }, { name: "Qr", link: "/qr" }, - { name: "Bill", link: "/bill" }, + { name: "Bill", link: "/bills" }, { name: "Settings", link: "/settings" }, ]; @@ -103,8 +103,8 @@ const page = () => {

{status === "authenticated" && ( - )} {status === "unauthenticated" && ( @@ -216,8 +216,8 @@ const page = () => {

{status === "authenticated" && ( - )} {status === "unauthenticated" && ( diff --git a/apps/merchant-app/app/qr/page.tsx b/apps/merchant-app/app/qr/page.tsx index 4c45cea..fde5d82 100644 --- a/apps/merchant-app/app/qr/page.tsx +++ b/apps/merchant-app/app/qr/page.tsx @@ -1,116 +1,9 @@ -"use client"; -import { useState } from "react"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { Button } from "@repo/ui/button"; -import { TextInput } from "@repo/ui/textinput"; +import QRPaymentHero from "../../components/molecules/QRPaymentHero"; export default function QRCodePage() { - const [amount, setAmount] = useState(""); - const [description, setDescription] = useState(""); - const [qrData, setQrData] = useState<{ - qrId: string; - qrCodeUrl: string; - } | null>(null); - - const generateQR = useMutation({ - mutationFn: async ({ - amount, - description, - }: { - amount: string; - description: string; - }) => { - const response = await axios.post("/api/qr/generate", { - amount: parseInt(amount) * 100, // Convert INR to paise - description, - }); - return response.data; - }, - onSuccess: (data) => { - setQrData({ qrId: data.qrId, qrCodeUrl: data.qrCodeUrl }); - }, - onError: (error: any) => { - const errorMsg = error.response?.data?.error || error.message || "Unknown error"; - alert(`Failed to generate QR code: ${errorMsg}`); - console.error("QR Generation Failed:", error); - }, - }); - - const { data: paymentStatus } = useQuery({ - queryKey: ["paymentStatus", qrData?.qrId], - queryFn: async () => { - if (!qrData?.qrId) return null; - const response = await axios.get(`/api/qr/status?qrId=${qrData.qrId}`); - return response.data.status; - }, - enabled: !!qrData?.qrId, - refetchInterval: 5000, - }); - - const handleGenerate = () => { - if (!amount || isNaN(parseInt(amount)) || parseInt(amount) < 1) { - alert("Please enter a valid amount (minimum ₹1)"); - return; - } - generateQR.mutate({ amount, description }); - }; - + return ( -
-

Generate QR Code for Payment

-
- setAmount(value)} - className="bg-zinc-700 text-zinc-100 border-zinc-600" - /> - setDescription(value.slice(0, 20))} - className="bg-zinc-700 text-zinc-100 border-zinc-600" - /> - - {qrData && ( -
- QR Code -

QR ID: {qrData.qrId}

-

Status: {paymentStatus || "PENDING"}

- {paymentStatus === "PENDING" && ( - - )} -
- )} -
-
+ ); } \ No newline at end of file diff --git a/apps/merchant-app/components.json b/apps/merchant-app/components.json new file mode 100644 index 0000000..c126d94 --- /dev/null +++ b/apps/merchant-app/components.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "app/globals.css", + "baseColor": "gray", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@aceternity": "https://ui.aceternity.com/registry/{name}.json" + } +} diff --git a/apps/merchant-app/components/atoms/Button.tsx b/apps/merchant-app/components/atoms/Button.tsx index 6546f75..6303a78 100644 --- a/apps/merchant-app/components/atoms/Button.tsx +++ b/apps/merchant-app/components/atoms/Button.tsx @@ -1,9 +1,11 @@ import { cn } from "../../lib/utils"; import { Loader2, Check } from "lucide-react"; -import { ButtonHTMLAttributes } from "react"; -import { motion } from "framer-motion"; +import { motion, HTMLMotionProps } from "framer-motion"; -interface Props extends ButtonHTMLAttributes { +import { ReactNode } from "react"; + +interface Props extends Omit, "ref" | "children"> { + children?: ReactNode; loading?: boolean; success?: boolean; variant?: "primary" | "secondary"; diff --git a/apps/merchant-app/components/molecules/QRPaymentHero.tsx b/apps/merchant-app/components/molecules/QRPaymentHero.tsx index b6b8129..17e675e 100644 --- a/apps/merchant-app/components/molecules/QRPaymentHero.tsx +++ b/apps/merchant-app/components/molecules/QRPaymentHero.tsx @@ -4,156 +4,136 @@ import { useState } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; import axios from "axios"; import { Button } from "../atoms/Button"; -import { FloatLabelInput } from "./FloatLabelInoput"; -import { LampContainer } from "@/components/ui/lamp"; -import { QRCodeSVG } from "qrcode.react"; import { motion } from "framer-motion"; -import { confetti } from "tsparticles-confetti"; -import { Badge } from "@repo/ui/badge"; +import { LampContainer } from "../ui/lamp"; +import { TextInput } from "@repo/ui/textinput"; export default function QRPaymentHero() { const [amount, setAmount] = useState(""); const [description, setDescription] = useState(""); - const [qrData, setQrData] = useState<{ qrId: string; qrCodeUrl: string } | null>(null); + const [qrData, setQrData] = useState<{ + qrId: string; + qrCodeUrl: string; + } | null>(null); const generateQR = useMutation({ - mutationFn: async ({ amount, description }: { amount: string; description: string }) => { - const res = await axios.post("/api/qr/generate", { - amount: parseInt(amount) * 100, + mutationFn: async ({ + amount, + description, + }: { + amount: string; + description: string; + }) => { + const response = await axios.post("/api/qr/generate", { + amount: parseInt(amount) * 100, // Convert INR to paise description, }); - return res.data; + return response.data; }, onSuccess: (data) => { setQrData({ qrId: data.qrId, qrCodeUrl: data.qrCodeUrl }); - confetti({ - particleCount: 100, - spread: 70, - origin: { y: 0.6 }, - colors: ["#06b6d4", "#3b82f6"], - }); + }, + onError: (error: any) => { + const errorMsg = error.response?.data?.error || error.message || "Unknown error"; + alert(`Failed to generate QR code: ${errorMsg}`); + console.error("QR Generation Failed:", error); }, }); - const { data: status } = useQuery({ - queryKey: ["status", qrData?.qrId], + const { data: paymentStatus } = useQuery({ + queryKey: ["paymentStatus", qrData?.qrId], queryFn: async () => { if (!qrData?.qrId) return null; - const res = await axios.get(`/api/qr/status?qrId=${qrData.qrId}`); - return res.data.status; + const response = await axios.get(`/api/qr/status?qrId=${qrData.qrId}`); + return response.data.status; }, enabled: !!qrData?.qrId, - refetchInterval: 4000, + refetchInterval: 5000, }); const handleGenerate = () => { - if (!amount || parseInt(amount) < 1) return alert("₹1 minimum"); + if (!amount || isNaN(parseInt(amount)) || parseInt(amount) < 1) { + alert("Please enter a valid amount (minimum ₹1)"); + return; + } generateQR.mutate({ amount, description }); }; + return ( - <> - - + + + +
+
+ {/* FORM — LEFT SIDE */} + setAmount(value)} + className="text-zinc-100 border-zinc-600" + /> + setDescription(value.slice(0, 20))} + className="text-zinc-100 border-zinc-600" + /> + - -
-
- - + QR Code - - -
- - {qrData && ( - -
- -
- -
- - {status || "PENDING"} - - {status === "PAID" && ( - - ₹{amount} Received! 🎉 - - )} -
- - {status === "PENDING" && ( - - )} -
- )} -
-
- - ); +
+

₹{amount}

+

7822952595@ibl

+

+ Status: {paymentStatus || "PENDING"} +

+
+ {paymentStatus === "PENDING" && ( + + )} + + )} +
+
+
+); } \ No newline at end of file diff --git a/apps/merchant-app/components/ui/lamp.tsx b/apps/merchant-app/components/ui/lamp.tsx new file mode 100644 index 0000000..fa536c6 --- /dev/null +++ b/apps/merchant-app/components/ui/lamp.tsx @@ -0,0 +1,104 @@ +"use client"; +import React from "react"; +import { motion } from "motion/react"; +import { cn } from "../../lib/utils"; + +export default function LampDemo() { + return ( + + + Build lamps
the right way +
+
+ ); +} + +export const LampContainer = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( +
+
+ +
+
+ + +
+
+ +
+
+
+ + + +
+
+ +
+ {children} +
+
+ ); +}; diff --git a/apps/merchant-app/package.json b/apps/merchant-app/package.json index 477e124..5de03c8 100644 --- a/apps/merchant-app/package.json +++ b/apps/merchant-app/package.json @@ -9,32 +9,48 @@ "lint": "eslint . --max-warnings 0" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.3", "@repo/db": "*", "@repo/store": "^1.0.0", "@repo/ui": "*", "@tanstack/react-query": "^5.90.5", "axios": "^1.12.2", + "canvas-confetti": "^1.9.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.543.0", + "motion": "^12.23.24", "next": "^14.1.1", "next-auth": "^4.24.7", "qrcode": "^1.5.4", "qrcode-generator": "^2.0.4", + "qrcode.react": "^4.2.0", "razorpay": "^2.9.6", "react": "^18.2.0", "react-dom": "^18.2.0", "react-qr-code": "^2.0.18", "recoil": "^0.7.7", + "sharp": "^0.34.4", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", + "tsparticles-confetti": "^2.12.0", "uuid": "^8.3.2" }, "devDependencies": { "@next/eslint-plugin-next": "^14.1.1", "@repo/eslint-config": "*", "@repo/typescript-config": "*", + "@types/canvas-confetti": "^1.9.0", "@types/eslint": "^8.56.5", "@types/node": "^20.11.24", "@types/qrcode": "^1.5.6", + "@types/qrcode.react": "^1.0.5", "@types/react": "^18.2.61", "@types/react-dom": "^18.2.19", + "autoprefixer": "^10.4.21", "eslint": "^8.57.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.18", "typescript": "^5.3.3" } } diff --git a/apps/merchant-app/tailwind.config.js b/apps/merchant-app/tailwind.config.js index b68cbc5..3ef6aab 100644 --- a/apps/merchant-app/tailwind.config.js +++ b/apps/merchant-app/tailwind.config.js @@ -1,13 +1,62 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ + darkMode: ["class"], + content: [ "./app/**/*.{js,ts,jsx,tsx,mdx}", "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "../../packages/ui/**/*.{js,ts,jsx,tsx,mdx}" ], theme: { - extend: {}, + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + } + } }, - plugins: [], + plugins: [require("tailwindcss-animate")], } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 34472ca..1e0a332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,32 +49,48 @@ "name": "web", "version": "1.0.0", "dependencies": { + "@radix-ui/react-slot": "^1.2.3", "@repo/db": "*", "@repo/store": "^1.0.0", "@repo/ui": "*", "@tanstack/react-query": "^5.90.5", "axios": "^1.12.2", + "canvas-confetti": "^1.9.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.543.0", + "motion": "^12.23.24", "next": "^14.1.1", "next-auth": "^4.24.7", "qrcode": "^1.5.4", "qrcode-generator": "^2.0.4", + "qrcode.react": "^4.2.0", "razorpay": "^2.9.6", "react": "^18.2.0", "react-dom": "^18.2.0", "react-qr-code": "^2.0.18", "recoil": "^0.7.7", + "sharp": "^0.34.4", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", + "tsparticles-confetti": "^2.12.0", "uuid": "^8.3.2" }, "devDependencies": { "@next/eslint-plugin-next": "^14.1.1", "@repo/eslint-config": "*", "@repo/typescript-config": "*", + "@types/canvas-confetti": "^1.9.0", "@types/eslint": "^8.56.5", "@types/node": "^20.11.24", "@types/qrcode": "^1.5.6", + "@types/qrcode.react": "^1.0.5", "@types/react": "^18.2.61", "@types/react-dom": "^18.2.19", + "autoprefixer": "^10.4.21", "eslint": "^8.57.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.18", "typescript": "^5.3.3" } }, @@ -588,7 +604,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1233,6 +1248,433 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/external-editor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", @@ -2974,6 +3416,13 @@ "@types/node": "*" } }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -3220,6 +3669,16 @@ "@types/node": "*" } }, + "node_modules/@types/qrcode.react": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/qrcode.react/-/qrcode.react-1.0.5.tgz", + "integrity": "sha512-BghPtnlwvrvq8QkGa1H25YnN+5OIgCKFuQruncGWLGJYOzeSKiix/4+B9BtfKF2wf5ja8yfyWYA3OXju995G8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -5311,6 +5770,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -12303,6 +12772,15 @@ "integrity": "sha512-mZSiP6RnbHl4xL2Ap5HfkjLnmxfKcPWpWe/c+5XxCuetEenqmNFf1FH/ftXPCtFG5/TDobjsjz6sSNL0Sr8Z9g==", "license": "MIT" }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -13401,6 +13879,48 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -14453,6 +14973,325 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsparticles-basic": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-basic/-/tsparticles-basic-2.12.0.tgz", + "integrity": "sha512-pN6FBpL0UsIUXjYbiui5+IVsbIItbQGOlwyGV55g6IYJBgdTNXgFX0HRYZGE9ZZ9psEXqzqwLM37zvWnb5AG9g==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0", + "tsparticles-move-base": "^2.12.0", + "tsparticles-shape-circle": "^2.12.0", + "tsparticles-updater-color": "^2.12.0", + "tsparticles-updater-opacity": "^2.12.0", + "tsparticles-updater-out-modes": "^2.12.0", + "tsparticles-updater-size": "^2.12.0" + } + }, + "node_modules/tsparticles-confetti": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-confetti/-/tsparticles-confetti-2.12.0.tgz", + "integrity": "sha512-PsxBL1DjYNNZecFFcymivnPypuxHKh0ePz2/9CctKl6zwS+Z8cHBCoszg8jBx6PJDJkAxIa76taezd54caISYg==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "tsparticles-basic": "^2.12.0", + "tsparticles-engine": "^2.12.0", + "tsparticles-plugin-emitters": "^2.12.0", + "tsparticles-plugin-motion": "^2.12.0", + "tsparticles-shape-cards": "^2.12.0", + "tsparticles-shape-heart": "^2.12.0", + "tsparticles-shape-image": "^2.12.0", + "tsparticles-shape-polygon": "^2.12.0", + "tsparticles-shape-square": "^2.12.0", + "tsparticles-shape-star": "^2.12.0", + "tsparticles-shape-text": "^2.12.0", + "tsparticles-updater-life": "^2.12.0", + "tsparticles-updater-roll": "^2.12.0", + "tsparticles-updater-rotate": "^2.12.0", + "tsparticles-updater-tilt": "^2.12.0", + "tsparticles-updater-wobble": "^2.12.0" + } + }, + "node_modules/tsparticles-engine": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-engine/-/tsparticles-engine-2.12.0.tgz", + "integrity": "sha512-ZjDIYex6jBJ4iMc9+z0uPe7SgBnmb6l+EJm83MPIsOny9lPpetMsnw/8YJ3xdxn8hV+S3myTpTN1CkOVmFv0QQ==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/tsparticles-move-base": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-move-base/-/tsparticles-move-base-2.12.0.tgz", + "integrity": "sha512-oSogCDougIImq+iRtIFJD0YFArlorSi8IW3HD2gO3USkH+aNn3ZqZNTqp321uB08K34HpS263DTbhLHa/D6BWw==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-plugin-emitters": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-plugin-emitters/-/tsparticles-plugin-emitters-2.12.0.tgz", + "integrity": "sha512-fbskYnaXWXivBh9KFReVCfqHdhbNQSK2T+fq2qcGEWpwtDdgujcaS1k2Q/xjZnWNMfVesik4IrqspcL51gNdSA==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-plugin-motion": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-plugin-motion/-/tsparticles-plugin-motion-2.12.0.tgz", + "integrity": "sha512-VeS0VDV5wc9a4t0xkPi3lkHqOvKRlELq4mEEvaIk8WwgOcx05TUZcJIIbhftnNabqgpHrZ4iUP5Nb2wZ3DBwWQ==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-shape-cards": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-cards/-/tsparticles-shape-cards-2.12.0.tgz", + "integrity": "sha512-4mSV1C7c/7SsSbS4A5HJEZE5tB2fOAEUXm52uagzBVMbL/YI+XkjOpi7L6JtCNcBKrWnZ/IgnnLMyyFGhNc4pA==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-shape-circle": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-circle/-/tsparticles-shape-circle-2.12.0.tgz", + "integrity": "sha512-L6OngbAlbadG7b783x16ns3+SZ7i0SSB66M8xGa5/k+YcY7zm8zG0uPt1Hd+xQDR2aNA3RngVM10O23/Lwk65Q==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-shape-heart": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-heart/-/tsparticles-shape-heart-2.12.0.tgz", + "integrity": "sha512-OK8CJrCY0Z6YAedyfTQh52u7KsurkP8eLNWDW11BhqcvDQkfwJC5g25Y3VrcW9Rwc88hrbNwFQlsKbY6tOn7qA==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-shape-image": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-image/-/tsparticles-shape-image-2.12.0.tgz", + "integrity": "sha512-iCkSdUVa40DxhkkYjYuYHr9MJGVw+QnQuN5UC+e/yBgJQY+1tQL8UH0+YU/h0GHTzh5Sm+y+g51gOFxHt1dj7Q==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-shape-polygon": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-polygon/-/tsparticles-shape-polygon-2.12.0.tgz", + "integrity": "sha512-5YEy7HVMt1Obxd/jnlsjajchAlYMr9eRZWN+lSjcFSH6Ibra7h59YuJVnwxOxAobpijGxsNiBX0PuGQnB47pmA==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-shape-square": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-square/-/tsparticles-shape-square-2.12.0.tgz", + "integrity": "sha512-33vfajHqmlODKaUzyPI/aVhnAOT09V7nfEPNl8DD0cfiNikEuPkbFqgJezJuE55ebtVo7BZPDA9o7GYbWxQNuw==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-shape-star": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-star/-/tsparticles-shape-star-2.12.0.tgz", + "integrity": "sha512-4sfG/BBqm2qBnPLASl2L5aBfCx86cmZLXeh49Un+TIR1F5Qh4XUFsahgVOG0vkZQa+rOsZPEH04xY5feWmj90g==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-shape-text": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-text/-/tsparticles-shape-text-2.12.0.tgz", + "integrity": "sha512-v2/FCA+hyTbDqp2ymFOe97h/NFb2eezECMrdirHWew3E3qlvj9S/xBibjbpZva2gnXcasBwxn0+LxKbgGdP0rA==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-color": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-color/-/tsparticles-updater-color-2.12.0.tgz", + "integrity": "sha512-KcG3a8zd0f8CTiOrylXGChBrjhKcchvDJjx9sp5qpwQK61JlNojNCU35xoaSk2eEHeOvFjh0o3CXWUmYPUcBTQ==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-life": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-life/-/tsparticles-updater-life-2.12.0.tgz", + "integrity": "sha512-J7RWGHAZkowBHpcLpmjKsxwnZZJ94oGEL2w+wvW1/+ZLmAiFFF6UgU0rHMC5CbHJT4IPx9cbkYMEHsBkcRJ0Bw==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-opacity": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-opacity/-/tsparticles-updater-opacity-2.12.0.tgz", + "integrity": "sha512-YUjMsgHdaYi4HN89LLogboYcCi1o9VGo21upoqxq19yRy0hRCtx2NhH22iHF/i5WrX6jqshN0iuiiNefC53CsA==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-out-modes": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-out-modes/-/tsparticles-updater-out-modes-2.12.0.tgz", + "integrity": "sha512-owBp4Gk0JNlSrmp12XVEeBroDhLZU+Uq3szbWlHGSfcR88W4c/0bt0FiH5bHUqORIkw+m8O56hCjbqwj69kpOQ==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-roll": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-roll/-/tsparticles-updater-roll-2.12.0.tgz", + "integrity": "sha512-dxoxY5jP4C9x15BxlUv5/Q8OjUPBiE09ToXRyBxea9aEJ7/iMw6odvi1HuT0H1vTIfV7o1MYawjeCbMycvODKQ==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-rotate": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-rotate/-/tsparticles-updater-rotate-2.12.0.tgz", + "integrity": "sha512-waOFlGFmEZOzsQg4C4VSejNVXGf4dMf3fsnQrEROASGf1FCd8B6WcZau7JtXSTFw0OUGuk8UGz36ETWN72DkCw==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-size": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-size/-/tsparticles-updater-size-2.12.0.tgz", + "integrity": "sha512-B0yRdEDd/qZXCGDL/ussHfx5YJ9UhTqNvmS5X2rR2hiZhBAE2fmsXLeWkdtF2QusjPeEqFDxrkGiLOsh6poqRA==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-tilt": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-tilt/-/tsparticles-updater-tilt-2.12.0.tgz", + "integrity": "sha512-HDEFLXazE+Zw+kkKKAiv0Fs9D9sRP61DoCR6jZ36ipea6OBgY7V1Tifz2TSR1zoQkk57ER9+EOQbkSQO+YIPGQ==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-wobble": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-wobble/-/tsparticles-updater-wobble-2.12.0.tgz", + "integrity": "sha512-85FIRl95ipD3jfIsQdDzcUC5PRMWIrCYqBq69nIy9P8rsNzygn+JK2n+P1VQZowWsZvk0mYjqb9OVQB21Lhf6Q==", + "deprecated": "starting from tsparticles v3 the packages are now moved to @tsparticles/package-name instead of tsparticles-package-name", + "license": "MIT", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, "node_modules/tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", diff --git a/packages/ui/src/TextInput.tsx b/packages/ui/src/TextInput.tsx index fc86644..39a1e46 100644 --- a/packages/ui/src/TextInput.tsx +++ b/packages/ui/src/TextInput.tsx @@ -19,8 +19,8 @@ export const TextInput = ({ required?: boolean; value?: string; }) => { - return
- + return
+ onChange(e.target.value)} type={type || "text"} id="first_name" className={cn("bg-zinc-600 border text-gray-100 text-sm rounded-lg outline-none focus:ring-zinc-500 selection:bg-zinc-300 w-full p-2.5", className)} placeholder={placeholder} required={required} value={value} />
; }; \ No newline at end of file