From 13756d175be50285de0f8e868c008724bbb83c2e Mon Sep 17 00:00:00 2001 From: Nasir Nadaf <109416738+VisibleNasir@users.noreply.github.com> Date: Sat, 8 Nov 2025 17:22:21 +0530 Subject: [PATCH] feat: Add recharge models and related migrations - Created migration scripts to drop existing `recharge_orders` and `recharge_plans` tables and create new `RechargeOrder` and `RechargePlan` tables with updated schemas. - Introduced new reward and referral models with migrations for `Reward` and `Referral` tables, including enums for reward types and statuses. - Added migration for refund requests with new `WrongSendRequest` table and updated `p2pTransfer` schema. - Updated Prisma schema to include new models and relationships for recharge, rewards, and referral systems. - Seeded initial data for rewards, including scratch cards, cashback, referral rewards, and milestone achievements. - Enhanced UI components for better styling and accessibility, including updates to `TextInput`, `AnimatedTestimonials`, `Avatar`, `Button`, and `SidebarLink`. --- apps/bank-webhook/package.json | 1 + apps/bank-webhook/src/index.ts | 222 +- .../app/{ => (dashboard)}/bills/page.tsx | 20 +- apps/merchant-app/app/(dashboard)/layout.tsx | 61 + .../app/{ => (dashboard)}/qr/page.tsx | 3 +- .../merchant-app/app/api/qr/generate/route.ts | 2 +- apps/merchant-app/app/home/page.tsx | 140 +- .../components/molecules/QRPaymentHero.tsx | 24 +- .../ui/background-beams-with-collision.tsx | 260 ++ .../components/ui/floating-navbar.tsx | 26 +- .../components/ui/shooting-stars.tsx | 147 + .../components/ui/stars-background.tsx | 144 + apps/user-app/app/(dashboard)/bills/page.tsx | 251 +- .../app/(dashboard)/dashboard/page.tsx | 142 +- apps/user-app/app/(dashboard)/layout.tsx | 62 +- apps/user-app/app/(dashboard)/loading.tsx | 14 + apps/user-app/app/(dashboard)/p2p/page.tsx | 47 +- .../app/(dashboard)/recharge/page.tsx | 64 + .../app/(dashboard)/return/[id]/page.tsx | 212 ++ .../user-app/app/(dashboard)/rewards/page.tsx | 29 + .../app/(dashboard)/transfer/page.tsx | 43 +- apps/user-app/app/api/p2p-history/route.ts | 26 + .../app/api/recharge/history/route.ts | 24 + .../app/api/recharge/initiate/route.ts | 88 + apps/user-app/app/api/recharge/plans/route.ts | 7 + apps/user-app/app/api/rewards/redeem/route.ts | 111 + apps/user-app/app/api/rewards/route.ts | 43 + .../user-app/app/api/rewards/scratch/route.ts | 48 + .../user-app/app/api/simulate-freeze/route.ts | 16 + .../app/api/webhooks/razorpay/route.ts | 24 + .../user-app/app/api/wrong-send/[id]/route.ts | 20 + .../app/api/wrong-send/approve/route.ts | 56 + apps/user-app/app/api/wrong-send/route.ts | 43 + .../app/auth/reset-password/new/page.tsx | 10 +- .../app/auth/reset-password/otp/page.tsx | 9 +- .../user-app/app/auth/reset-password/page.tsx | 13 +- apps/user-app/app/auth/signin/page.tsx | 23 +- apps/user-app/app/auth/signup/page.tsx | 48 +- apps/user-app/app/globals.css | 13 + apps/user-app/app/home/page.tsx | 179 +- apps/user-app/app/layout.tsx | 9 +- apps/user-app/app/lib/rewards.ts | 41 + apps/user-app/components/AddMoneyCard.tsx | 73 +- apps/user-app/components/BalanceCard.tsx | 45 +- apps/user-app/components/BillPaymentCard.tsx | 56 +- apps/user-app/components/OnRampList.tsx | 8 +- .../components/P2PTransactionHistory.tsx | 211 +- .../components/P2pTransationsHistory.tsx | 97 +- apps/user-app/components/RechargeForm.tsx | 265 ++ .../user-app/components/ReturnPendingList.tsx | 157 ++ apps/user-app/components/RewardsDash.tsx | 201 ++ apps/user-app/components/SendCard.tsx | 18 +- apps/user-app/components/StatsCards.tsx | 83 +- apps/user-app/components/WrongSendModal.tsx | 47 + .../ui/background-beams-with-collision.tsx | 259 ++ apps/user-app/components/ui/button.tsx | 6 +- apps/user-app/components/ui/card.tsx | 2 +- .../components/ui/floating-navbar.tsx | 22 +- apps/user-app/components/ui/glare-card.tsx | 133 + apps/user-app/components/ui/hero-parallax.tsx | 18 +- .../components/ui/hover-border-gradient.tsx | 4 +- apps/user-app/components/ui/input.tsx | 2 +- apps/user-app/components/ui/progress.tsx | 28 + apps/user-app/components/ui/select.tsx | 6 +- .../user-app/components/ui/shooting-stars.tsx | 146 + .../components/ui/stars-background.tsx | 143 + apps/user-app/components/ui/table.tsx | 120 + .../user-app/components/ui/theme-provider.tsx | 11 + apps/user-app/cron/panelty.ts | 42 + apps/user-app/lib/firebase.ts | 2 +- apps/user-app/next-env.d.ts | 3 +- apps/user-app/package.json | 7 +- package-lock.json | 2406 +++++++++++++++-- package.json | 1 + packages/db/prisma/RePlans.js | 131 + .../migration.sql | 43 + .../migration.sql | 55 + .../migration.sql | 62 + .../migration.sql | 50 + .../migration.sql | 54 + packages/db/prisma/schema.prisma | 118 +- packages/db/prisma/seed-rewards.ts | 77 + packages/db/prisma/seed.ts | 308 +-- packages/ui/src/TextInput.tsx | 2 +- packages/ui/src/animated-testimonials.tsx | 10 +- packages/ui/src/avatar.tsx | 2 +- packages/ui/src/button.tsx | 2 +- packages/ui/src/sidebar.tsx | 9 +- 88 files changed, 7055 insertions(+), 1255 deletions(-) rename apps/merchant-app/app/{ => (dashboard)}/bills/page.tsx (82%) create mode 100644 apps/merchant-app/app/(dashboard)/layout.tsx rename apps/merchant-app/app/{ => (dashboard)}/qr/page.tsx (52%) create mode 100644 apps/merchant-app/components/ui/background-beams-with-collision.tsx create mode 100644 apps/merchant-app/components/ui/shooting-stars.tsx create mode 100644 apps/merchant-app/components/ui/stars-background.tsx create mode 100644 apps/user-app/app/(dashboard)/loading.tsx create mode 100644 apps/user-app/app/(dashboard)/recharge/page.tsx create mode 100644 apps/user-app/app/(dashboard)/return/[id]/page.tsx create mode 100644 apps/user-app/app/(dashboard)/rewards/page.tsx create mode 100644 apps/user-app/app/api/p2p-history/route.ts create mode 100644 apps/user-app/app/api/recharge/history/route.ts create mode 100644 apps/user-app/app/api/recharge/initiate/route.ts create mode 100644 apps/user-app/app/api/recharge/plans/route.ts create mode 100644 apps/user-app/app/api/rewards/redeem/route.ts create mode 100644 apps/user-app/app/api/rewards/route.ts create mode 100644 apps/user-app/app/api/rewards/scratch/route.ts create mode 100644 apps/user-app/app/api/simulate-freeze/route.ts create mode 100644 apps/user-app/app/api/webhooks/razorpay/route.ts create mode 100644 apps/user-app/app/api/wrong-send/[id]/route.ts create mode 100644 apps/user-app/app/api/wrong-send/approve/route.ts create mode 100644 apps/user-app/app/api/wrong-send/route.ts create mode 100644 apps/user-app/app/lib/rewards.ts create mode 100644 apps/user-app/components/RechargeForm.tsx create mode 100644 apps/user-app/components/ReturnPendingList.tsx create mode 100644 apps/user-app/components/RewardsDash.tsx create mode 100644 apps/user-app/components/WrongSendModal.tsx create mode 100644 apps/user-app/components/ui/background-beams-with-collision.tsx create mode 100644 apps/user-app/components/ui/glare-card.tsx create mode 100644 apps/user-app/components/ui/progress.tsx create mode 100644 apps/user-app/components/ui/shooting-stars.tsx create mode 100644 apps/user-app/components/ui/stars-background.tsx create mode 100644 apps/user-app/components/ui/table.tsx create mode 100644 apps/user-app/components/ui/theme-provider.tsx create mode 100644 apps/user-app/cron/panelty.ts create mode 100644 packages/db/prisma/RePlans.js create mode 100644 packages/db/prisma/migrations/20251104141035_add_recharge_models/migration.sql create mode 100644 packages/db/prisma/migrations/20251104142535_add_recharge_models1/migration.sql create mode 100644 packages/db/prisma/migrations/20251104143227_add_recharge_models/migration.sql create mode 100644 packages/db/prisma/migrations/20251104193507_add_reward_referal/migration.sql create mode 100644 packages/db/prisma/migrations/20251104220423_refund_request/migration.sql create mode 100644 packages/db/prisma/seed-rewards.ts diff --git a/apps/bank-webhook/package.json b/apps/bank-webhook/package.json index 55a0da9..6b1d789 100644 --- a/apps/bank-webhook/package.json +++ b/apps/bank-webhook/package.json @@ -15,6 +15,7 @@ "@repo/db": "*", "@types/express": "^4.17.21", "@types/ws": "^8.18.1", + "axios": "^1.13.2", "cors": "^2.8.5", "crypto": "^1.0.1", "esbuild": "^0.25.10", diff --git a/apps/bank-webhook/src/index.ts b/apps/bank-webhook/src/index.ts index d321e9f..28a6255 100644 --- a/apps/bank-webhook/src/index.ts +++ b/apps/bank-webhook/src/index.ts @@ -3,85 +3,147 @@ import cors from "cors"; import crypto from "crypto"; import db from "@repo/db/client"; import prisma from "@repo/db/client"; +import axios from "axios"; const app = express(); app.use(cors({ origin: ["http://localhost:3001"] })); app.use(express.json()); app.post("/hdfcWebhook", async (req, res) => { - const { token, amount, status, type } = req.body; + const { token, amount, status, type } = req.body; - try { - if (type === "ONRAMP") { - const transaction = await db.onRampTransaction.findUnique({ - where: { token } - }); - - if (!transaction) { - return res.status(404).json({ message: "Transaction not found" }); - } - - const paymentInformation = { - token, - userId: transaction.userId, - amount: Number(amount) - }; - - const [balanceResult, transactionResult] = await db.$transaction([ - db.balance.upsert({ - where: { userId: paymentInformation.userId }, - update: { amount: { increment: paymentInformation.amount } }, - create: { - userId: paymentInformation.userId, - amount: paymentInformation.amount, - locked: 0 - } - }), - db.onRampTransaction.update({ - where: { token: paymentInformation.token }, - data: { status: status === "SUCCESS" ? "Success" : "Failure" } - }) - ]); - - console.log("✅ Balance:", balanceResult.amount); - console.log("✅ Transaction:", transactionResult.status); - } else if (type === "BILL") { - const bill = await db.billSchedule.findUnique({ - where: { token } - }); - - if (!bill) { - return res.status(404).json({ message: "Bill not found" }); - } - - const billStatus = status === "SUCCESS" ? "PAID" : "OVERDUE"; - await db.billSchedule.update({ - where: { token }, - data: { status: billStatus } - }); - - console.log("✅ Bill:", bill.id, billStatus); - - const notifyEndpoints = [ - "http://localhost:3001/api/bills/notify" - ]; - - for (const endpoint of notifyEndpoints) { - await fetch(endpoint, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ billId: bill.id, status: billStatus, token }) - }); - } - } else { - return res.status(400).json({ message: "Invalid type" }); - } - - res.json({ message: "Captured" }); - } catch (e) { - console.error("❌ Webhook error:", e); - res.status(500).json({ message: "Processing failed" }); + try { + if (type === "ONRAMP") { + const transaction = await db.onRampTransaction.findUnique({ + where: { token }, + }); + + if (!transaction) { + return res.status(404).json({ message: "Transaction not found" }); + } + + const paymentInformation = { + token, + userId: transaction.userId, + amount: Number(amount), + }; + + const [balanceResult, transactionResult] = await db.$transaction([ + db.balance.upsert({ + where: { userId: paymentInformation.userId }, + update: { amount: { increment: paymentInformation.amount } }, + create: { + userId: paymentInformation.userId, + amount: paymentInformation.amount, + locked: 0, + }, + }), + db.onRampTransaction.update({ + where: { token: paymentInformation.token }, + data: { status: status === "SUCCESS" ? "Success" : "Failure" }, + }), + ]); + + console.log("✅ Balance:", balanceResult.amount); + console.log("✅ Transaction:", transactionResult.status); + } else if (type === "BILL") { + const bill = await db.billSchedule.findUnique({ + where: { token }, + }); + + if (!bill) { + return res.status(404).json({ message: "Bill not found" }); + } + + const billStatus = status === "SUCCESS" ? "PAID" : "OVERDUE"; + await db.billSchedule.update({ + where: { token }, + data: { status: billStatus }, + }); + + console.log("✅ Bill:", bill.id, billStatus); + + const notifyEndpoints = ["http://localhost:3001/api/bills/notify"]; + + for (const endpoint of notifyEndpoints) { + await fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ billId: bill.id, status: billStatus, token }), + }); + } + } else if (type === "P2P") { + const transaction = await db.p2pTransfer.findUnique({ + where: { id: token }, + }); + + if (!transaction) + return res.status(404).json({ message: "P2P not found" }); + + if (status === "SUCCESS") { + await db.$transaction([ + db.balance.upsert({ + where: { userId: transaction.toUserId! }, + update: { amount: { increment: transaction.amount } }, + create: { + userId: transaction.toUserId!, + amount: transaction.amount, + locked: 0, + }, + }), + db.p2pTransfer.update({ + where: { id: transaction.id }, + data: { status: "SUCCESS" }, + }), + ]); + } + } else if (type === "FREEZE") { + // Find the wrong-send request by the bank token stored on the wrongSendRequest record. + // (Querying transaction.bankToken failed because p2pTransfer doesn't have that field.) + const request = await db.wrongSendRequest.findFirst({ + where: { txnId : Number(token) }, + include: { + sender: { select: { id: true, name: true, number: true } }, + transaction: true, + }, + }); + + if (!request) return res.status(404).json({ message: "Request not found" }); + + // Safe access (use optional chaining). convert paise -> rupees for message. + const amountRupees = Number(request.amount) / 100; + const sender = (request as any).sender; + const senderName = sender?.name ?? "Someone"; + const senderNumber = sender?.number; + const receiverNumber = request.receiverNumber; + + // Notify receiver via SMS + if (receiverNumber) { + const msg = `${senderName} sent ₹${amountRupees} by mistake. Return in 24 hrs or ₹50 fee: yourapp.com/return/${request.id}`; + await axios.get( + `https://www.fast2sms.com/dev/bulkV2?authorization=${process.env.FAST2SMS}&message=${encodeURIComponent( + msg + )}&numbers=${receiverNumber}` + ); + } + + // Notify sender + if (senderNumber) { + await axios.get( + `https://www.fast2sms.com/dev/bulkV2?authorization=${process.env.FAST2SMS}&message=${encodeURIComponent( + `Bank frozen ₹${amountRupees}. Receiver notified.` + )}&numbers=${senderNumber}` + ); + } + } else { + return res.status(400).json({ message: "Invalid type" }); } + + res.json({ message: "Captured" }); + } catch (e) { + console.error("❌ Webhook error:", e); + res.status(500).json({ message: "Processing failed" }); + } }); app.post("/webhook/upi-payment", async (req, res) => { @@ -91,7 +153,9 @@ app.post("/webhook/upi-payment", async (req, res) => { return res.status(400).json({ error: "Missing qrId or transactionId" }); } - const payment = await prisma.merchantPayment.findUnique({ where: { qrId } }); + const payment = await prisma.merchantPayment.findUnique({ + where: { qrId }, + }); if (!payment) { return res.status(404).json({ error: "Payment not found" }); } @@ -128,7 +192,9 @@ app.post("/webhook/qr-payment", async (req, res) => { if (event === "qr_code.paid") { const { qr_id, amount, payment_id } = req.body.payload.qr_code.entity; try { - const payment = await prisma.merchantPayment.findUnique({ where: { qrId: qr_id } }); + const payment = await prisma.merchantPayment.findUnique({ + where: { qrId: qr_id }, + }); if (!payment) { return res.status(404).json({ error: "Payment not found" }); } @@ -140,11 +206,15 @@ app.post("/webhook/qr-payment", async (req, res) => { }); // Transfer amount to merchant (simplified, update merchant balance or trigger transfer) - const merchant = await prisma.merchant.findUnique({ where: { id: payment.merchantId } }); + const merchant = await prisma.merchant.findUnique({ + where: { id: payment.merchantId }, + }); // Add logic to update merchant balance or initiate payout if needed // Trigger notification (e.g., via email or push notification) - console.log(`Payment of ₹${amount / 100} successful for merchant ${merchant?.name}`); + console.log( + `Payment of ₹${amount / 100} successful for merchant ${merchant?.name}` + ); res.status(200).json({ status: "success" }); } catch (error) { @@ -161,4 +231,4 @@ app.get("/health", (req, res) => { }); const PORT = process.env.PORT || 3002; -app.listen(PORT, () => console.log(`Webhook backend running on port ${PORT}`)); \ No newline at end of file +app.listen(PORT, () => console.log(`Webhook backend running on port ${PORT}`)); diff --git a/apps/merchant-app/app/bills/page.tsx b/apps/merchant-app/app/(dashboard)/bills/page.tsx similarity index 82% rename from apps/merchant-app/app/bills/page.tsx rename to apps/merchant-app/app/(dashboard)/bills/page.tsx index 4d072f0..bc9cef5 100644 --- a/apps/merchant-app/app/bills/page.tsx +++ b/apps/merchant-app/app/(dashboard)/bills/page.tsx @@ -40,7 +40,7 @@ export default function MerchantBillsPage() { }; return ( -
+
{/* HEADER */} Merchant Bill Dashboard -

Total Bills: {bills.length}

+

Total Bills: {bills.length}

- {/* BILLS GRID */} -
+
+ {/* BILLS GRID */} +
{bills.map((bill) => { const due = new Date(bill.dueDate); const overdue = due < new Date() && bill.status !== "PAID"; @@ -64,10 +65,10 @@ export default function MerchantBillsPage() {
-

{bill.billType}

+

{bill.billType}

@@ -109,6 +110,9 @@ export default function MerchantBillsPage() { ); })}
+ +
+
); diff --git a/apps/merchant-app/app/(dashboard)/layout.tsx b/apps/merchant-app/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..a4ae300 --- /dev/null +++ b/apps/merchant-app/app/(dashboard)/layout.tsx @@ -0,0 +1,61 @@ +"use client"; +import { useSession } from "next-auth/react"; +import { Home, LayoutDashboard, Send, Repeat, BrickWallFire, Wallet } from 'lucide-react'; +import { Avatar, AvatarFallback, AvatarImage } from '../../../../packages/ui/src/avatar'; +import { TooltipProvider } from '../../../../packages/ui/src/tooltip'; +import { Sidebar, SidebarBody, SidebarLink, SignupBtn } from '../../../../packages/ui/src/sidebar'; +import { useMemo } from 'react'; + +interface Links { + label: string; + href: string; + icon: React.JSX.Element; +} + +export default function Layout({ children }: { children: React.ReactNode }) { + const { data: session } = useSession(); + + const sidebarLinks = useMemo(() => [ + { label: "Home", href: "/", icon: }, + { label: "QR", href: "/qr", icon: }, + { label: "Bills", href: "/bills", icon: }, + ], []); + + return ( + +
+ + + + +
+
+ + + {session?.user?.name?.[0] || "US"} + +
+

+ {session?.user?.name || "Your Name"} +

+

+ {session?.user?.email || "abc@example.com"} +

+
+ +
+
+
+
+ +
+ {children} +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/merchant-app/app/qr/page.tsx b/apps/merchant-app/app/(dashboard)/qr/page.tsx similarity index 52% rename from apps/merchant-app/app/qr/page.tsx rename to apps/merchant-app/app/(dashboard)/qr/page.tsx index fde5d82..85b2dcf 100644 --- a/apps/merchant-app/app/qr/page.tsx +++ b/apps/merchant-app/app/(dashboard)/qr/page.tsx @@ -1,5 +1,4 @@ - -import QRPaymentHero from "../../components/molecules/QRPaymentHero"; +import QRPaymentHero from "../../../components/molecules/QRPaymentHero"; export default function QRCodePage() { diff --git a/apps/merchant-app/app/api/qr/generate/route.ts b/apps/merchant-app/app/api/qr/generate/route.ts index 8765c2a..c24c588 100644 --- a/apps/merchant-app/app/api/qr/generate/route.ts +++ b/apps/merchant-app/app/api/qr/generate/route.ts @@ -35,7 +35,7 @@ export async function POST(request: Request) { console.error(`Merchant not found for email: ${session.user.email}`); return NextResponse.json({ error: `Merchant not found for email: ${session.user.email}` }, { status: 404 }); } - + const upiId = "7822952595@ibl"; const merchantName = merchant.name && merchant.name.length <= 15 ? merchant.name : "Merchant"; const qrId = generateShortId(); diff --git a/apps/merchant-app/app/home/page.tsx b/apps/merchant-app/app/home/page.tsx index ce27b8d..cd5e30a 100644 --- a/apps/merchant-app/app/home/page.tsx +++ b/apps/merchant-app/app/home/page.tsx @@ -6,7 +6,6 @@ import React from 'react' import { FloatingNav } from '../../components/ui/floating-navbar'; import { ContainerTextFlip } from '../../components/ui/container-text-flip'; import { HoverBorderGradient } from '../../components/ui/hover-border-gradient'; -import { FeaturesSectionDemo } from '../../components/ui/FeatureSection'; import { motion } from 'motion/react'; import { AnimatedTestimonials } from '@repo/ui/animated-testimonials'; import { PointerHighlight } from '../../components/ui/pointer-highlight'; @@ -17,13 +16,14 @@ import roni from '../../public/roni.jpeg'; import nasir from '../../public/nasir.jpg'; import tan from '../../public/tan.jpeg'; import vikas from '../../public/vikas.jpeg'; - -const page = () => { - const navItems = [ - { name: "Home", link: "/" }, - { name: "Qr", link: "/qr" }, - { name: "Bill", link: "/bills" }, - { name: "Settings", link: "/settings" }, +import { ShootingStars } from '../../components/ui/shooting-stars'; +import { StarsBackground } from '../../components/ui/stars-background'; +import { BackgroundBeamsWithCollision } from '../../components/ui/background-beams-with-collision'; + +const Page = () => { + const navItems = [ + { name: "qr", link: "/qr" }, + { name: "Bills", link: "/bills" }, ]; const team = [ @@ -57,7 +57,6 @@ const page = () => { }, ]; - const S1 = [ { icon: "🔐", @@ -80,81 +79,58 @@ const page = () => { description: "PCI DSS & GDPR certified", }, ]; - + const { status } = useSession(); const router = useRouter(); + return ( -
+
{/* Floating Navbar */} {/* Background with content */} -
-
-

- — with - confidence. -

- -

- Experience effortless payments built for security and speed. - CalxSecure empowers you to handle every transaction with total - trust. -

- - {status === "authenticated" && ( - - )} - {status === "unauthenticated" && ( - - )} - -
- - -
- - {/* Features Section */} -
-
- -
-
-

- Powerful Features for Modern Finance -

- -

- CalxSecure handles everything from peer-to-peer transactions to - automated bank webhooks — built to scale beyond a million users. + +

+
+

+ — with + confidence. +

+ +

+ Experience effortless payments built for security and speed. + CalxSecure empowers you to handle every transaction with total + trust.

+ + {status === "authenticated" && ( + + )} + {status === "unauthenticated" && ( + + )} +
- -
-
+ {/* Security Section */} -
- {/* Background Pattern */} -
-
-
- -
+
+
{/* Header */} -
+ +
🔒 Bank-Grade Protection

Enterprise-Level Security

-

+

Your funds are protected with military-grade encryption, AI-powered fraud detection, and 24/7 monitoring—trusted by millions worldwide. @@ -162,7 +138,7 @@ const page = () => {

{/* Features Grid */} -
+
{S1.map((item, index) => ( { viewport={{ once: true }} className="group relative" > -
+
{/* Icon */}
@@ -183,28 +159,30 @@ const page = () => {
{/* Content */} -

+

{item.title}

-

+

{item.description || "Advanced protection"}

- -
))} + +
-
-
+
+

+ Members +

{/* CTA Section */} -
+
Ready to Get @@ -214,10 +192,10 @@ const page = () => { Join thousands of users and merchants powering their payments with CalxSecure

- + {status === "authenticated" && ( - )} {status === "unauthenticated" && ( @@ -230,11 +208,11 @@ const page = () => {
{/* Footer */} -
+

© 2025 CalxSecure. All rights reserved.

- ) -} + ); +}; -export default page +export default Page; diff --git a/apps/merchant-app/components/molecules/QRPaymentHero.tsx b/apps/merchant-app/components/molecules/QRPaymentHero.tsx index 17e675e..11e3812 100644 --- a/apps/merchant-app/components/molecules/QRPaymentHero.tsx +++ b/apps/merchant-app/components/molecules/QRPaymentHero.tsx @@ -7,6 +7,7 @@ import { Button } from "../atoms/Button"; import { motion } from "framer-motion"; import { LampContainer } from "../ui/lamp"; import { TextInput } from "@repo/ui/textinput"; +import { HoverBorderGradient } from "../ui/hover-border-gradient"; export default function QRPaymentHero() { const [amount, setAmount] = useState(""); @@ -61,15 +62,15 @@ export default function QRPaymentHero() { return ( - + -
+
{/* FORM — LEFT SIDE */} setAmount(value)} - className="text-zinc-100 border-zinc-600" + className=" border-zinc-400 dark:border-zinc-600" /> setDescription(value.slice(0, 20))} - className="text-zinc-100 border-zinc-600" + className="border-zinc-400 dark:border-zinc-600" /> - + + + + {/* QR — APPEARS ON RIGHT SIDE */} {qrData && ( @@ -107,7 +111,7 @@ export default function QRPaymentHero() { className="w-72 h-72" style={{ backgroundColor: "#27272A" }} /> -
+

₹{amount}

7822952595@ibl

@@ -125,7 +129,7 @@ export default function QRPaymentHero() { .catch((err) => alert(`Confirmation failed: ${err.response?.data?.error || err.message}`)); } }} - className="mt-4 w-full bg-green-600 hover:bg-green-700" + className="mt-4 w-full bg-zinc-600 hover:bg-zinc-700" > Confirm Payment diff --git a/apps/merchant-app/components/ui/background-beams-with-collision.tsx b/apps/merchant-app/components/ui/background-beams-with-collision.tsx new file mode 100644 index 0000000..2339224 --- /dev/null +++ b/apps/merchant-app/components/ui/background-beams-with-collision.tsx @@ -0,0 +1,260 @@ +"use client"; + +import { motion, AnimatePresence } from "motion/react"; +import React, { useRef, useState, useEffect } from "react"; +import { cn } from "../../lib/utils"; + +export const BackgroundBeamsWithCollision = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + const containerRef = useRef(null); + const parentRef = useRef(null); + + const beams = [ + { + initialX: 10, + translateX: 10, + duration: 7, + repeatDelay: 3, + delay: 2, + }, + { + initialX: 600, + translateX: 600, + duration: 3, + repeatDelay: 3, + delay: 4, + }, + { + initialX: 100, + translateX: 100, + duration: 7, + repeatDelay: 7, + className: "h-6", + }, + { + initialX: 400, + translateX: 400, + duration: 5, + repeatDelay: 14, + delay: 4, + }, + { + initialX: 800, + translateX: 800, + duration: 11, + repeatDelay: 2, + className: "h-20", + }, + { + initialX: 1000, + translateX: 1000, + duration: 4, + repeatDelay: 2, + className: "h-12", + }, + { + initialX: 1200, + translateX: 1200, + duration: 6, + repeatDelay: 4, + delay: 2, + className: "h-6", + }, + ]; + + return ( +

+ {beams.map((beam) => ( + + ))} + + {children} +
+
+ ); +}; + +const CollisionMechanism = React.forwardRef< + HTMLDivElement, + { + containerRef: React.RefObject; + parentRef: React.RefObject; + beamOptions?: { + initialX?: number; + translateX?: number; + initialY?: number; + translateY?: number; + rotate?: number; + className?: string; + duration?: number; + delay?: number; + repeatDelay?: number; + }; + } +>(({ parentRef, containerRef, beamOptions = {} }, ref) => { + const beamRef = useRef(null); + const [collision, setCollision] = useState<{ + detected: boolean; + coordinates: { x: number; y: number } | null; + }>({ + detected: false, + coordinates: null, + }); + const [beamKey, setBeamKey] = useState(0); + const [cycleCollisionDetected, setCycleCollisionDetected] = useState(false); + + useEffect(() => { + const checkCollision = () => { + if ( + beamRef.current && + containerRef.current && + parentRef.current && + !cycleCollisionDetected + ) { + const beamRect = beamRef.current.getBoundingClientRect(); + const containerRect = containerRef.current.getBoundingClientRect(); + const parentRect = parentRef.current.getBoundingClientRect(); + + if (beamRect.bottom >= containerRect.top) { + const relativeX = + beamRect.left - parentRect.left + beamRect.width / 2; + const relativeY = beamRect.bottom - parentRect.top; + + setCollision({ + detected: true, + coordinates: { + x: relativeX, + y: relativeY, + }, + }); + setCycleCollisionDetected(true); + } + } + }; + + const animationInterval = setInterval(checkCollision, 50); + + return () => clearInterval(animationInterval); + }, [cycleCollisionDetected, containerRef]); + + useEffect(() => { + if (collision.detected && collision.coordinates) { + setTimeout(() => { + setCollision({ detected: false, coordinates: null }); + setCycleCollisionDetected(false); + }, 2000); + + setTimeout(() => { + setBeamKey((prevKey) => prevKey + 1); + }, 2000); + } + }, [collision]); + + return ( + <> + + + {collision.detected && collision.coordinates && ( + + )} + + + ); +}); + +CollisionMechanism.displayName = "CollisionMechanism"; + +const Explosion = ({ ...props }: React.HTMLProps) => { + const spans = Array.from({ length: 20 }, (_, index) => ({ + id: index, + initialX: 0, + initialY: 0, + directionX: Math.floor(Math.random() * 80 - 40), + directionY: Math.floor(Math.random() * -50 - 10), + })); + + return ( +
+ + {spans.map((span) => ( + + ))} +
+ ); +}; diff --git a/apps/merchant-app/components/ui/floating-navbar.tsx b/apps/merchant-app/components/ui/floating-navbar.tsx index 2a38e1b..e0b38e2 100644 --- a/apps/merchant-app/components/ui/floating-navbar.tsx +++ b/apps/merchant-app/components/ui/floating-navbar.tsx @@ -1,7 +1,6 @@ "use client"; import React from "react"; import { motion } from "motion/react"; -import { cn } from "../../lib/utils"; import { signOut, useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; @@ -9,6 +8,11 @@ import { Avatar, AvatarFallback, AvatarImage } from "../../../../packages/ui/src import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "./dropdown-menu"; import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu"; import { TextHoverEffect } from "../../../../packages/ui/src/text-hover-effect"; +import { useTheme } from "next-themes"; +import { Moon, Sun } from "lucide-react"; +import Link from "next/link"; +import { cn } from "../../lib/utils"; +import { Button } from "@repo/ui/button"; export const FloatingNav = ({ navItems, @@ -21,6 +25,7 @@ export const FloatingNav = ({ }[]; className?: string; }) => { + const { theme , setTheme } = useTheme(); const { data: session, status } = useSession(); const router = useRouter(); @@ -49,7 +54,7 @@ export const FloatingNav = ({ > {navItems.map((navItem, idx) => ( - {navItem.icon && {navItem.icon}} {navItem.name} - + ))} { status === "unauthenticated" && ( @@ -72,7 +77,7 @@ export const FloatingNav = ({ - + {session?.user?.name?.[0] || "US"} @@ -84,9 +89,20 @@ export const FloatingNav = ({ + )} - + + {theme === "light" ? ( + + ) : ( + + )} +
); diff --git a/apps/merchant-app/components/ui/shooting-stars.tsx b/apps/merchant-app/components/ui/shooting-stars.tsx new file mode 100644 index 0000000..9bac384 --- /dev/null +++ b/apps/merchant-app/components/ui/shooting-stars.tsx @@ -0,0 +1,147 @@ +"use client"; + +import React, { useEffect, useState, useRef } from "react"; +import { cn } from "../../lib/utils"; + +interface ShootingStar { + id: number; + x: number; + y: number; + angle: number; + scale: number; + speed: number; + distance: number; +} + +interface ShootingStarsProps { + minSpeed?: number; + maxSpeed?: number; + minDelay?: number; + maxDelay?: number; + starColor?: string; + trailColor?: string; + starWidth?: number; + starHeight?: number; + className?: string; +} + +const getRandomStartPoint = () => { + const side = Math.floor(Math.random() * 4); + const offset = Math.random() * window.innerWidth; + + switch (side) { + case 0: + return { x: offset, y: 0, angle: 45 }; + case 1: + return { x: window.innerWidth, y: offset, angle: 135 }; + case 2: + return { x: offset, y: window.innerHeight, angle: 225 }; + case 3: + return { x: 0, y: offset, angle: 315 }; + default: + return { x: 0, y: 0, angle: 45 }; + } +}; +export const ShootingStars: React.FC = ({ + minSpeed = 10, + maxSpeed = 30, + minDelay = 1200, + maxDelay = 4200, + starColor = "#9E00FF", + trailColor = "#2EB9DF", + starWidth = 10, + starHeight = 1, + className, +}) => { + const [star, setStar] = useState(null); + const svgRef = useRef(null); + + useEffect(() => { + const createStar = () => { + const { x, y, angle } = getRandomStartPoint(); + const newStar: ShootingStar = { + id: Date.now(), + x, + y, + angle, + scale: 1, + speed: Math.random() * (maxSpeed - minSpeed) + minSpeed, + distance: 0, + }; + setStar(newStar); + + const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay; + setTimeout(createStar, randomDelay); + }; + + createStar(); + + return () => {}; + }, [minSpeed, maxSpeed, minDelay, maxDelay]); + + useEffect(() => { + const moveStar = () => { + if (star) { + setStar((prevStar) => { + if (!prevStar) return null; + const newX = + prevStar.x + + prevStar.speed * Math.cos((prevStar.angle * Math.PI) / 180); + const newY = + prevStar.y + + prevStar.speed * Math.sin((prevStar.angle * Math.PI) / 180); + const newDistance = prevStar.distance + prevStar.speed; + const newScale = 1 + newDistance / 100; + if ( + newX < -20 || + newX > window.innerWidth + 20 || + newY < -20 || + newY > window.innerHeight + 20 + ) { + return null; + } + return { + ...prevStar, + x: newX, + y: newY, + distance: newDistance, + scale: newScale, + }; + }); + } + }; + + const animationFrame = requestAnimationFrame(moveStar); + return () => cancelAnimationFrame(animationFrame); + }, [star]); + + return ( + + {star && ( + + )} + + + + + + + + ); +}; diff --git a/apps/merchant-app/components/ui/stars-background.tsx b/apps/merchant-app/components/ui/stars-background.tsx new file mode 100644 index 0000000..368076d --- /dev/null +++ b/apps/merchant-app/components/ui/stars-background.tsx @@ -0,0 +1,144 @@ +"use client"; + +import React, { + useState, + useEffect, + useRef, + RefObject, + useCallback, +} from "react"; +import { cn } from "../../lib/utils"; + +interface StarProps { + x: number; + y: number; + radius: number; + opacity: number; + twinkleSpeed: number | null; +} + +interface StarBackgroundProps { + starDensity?: number; + allStarsTwinkle?: boolean; + twinkleProbability?: number; + minTwinkleSpeed?: number; + maxTwinkleSpeed?: number; + className?: string; +} + +export const StarsBackground: React.FC = ({ + starDensity = 0.00015, + allStarsTwinkle = true, + twinkleProbability = 0.7, + minTwinkleSpeed = 0.5, + maxTwinkleSpeed = 1, + className, +}) => { + const [stars, setStars] = useState([]); + const canvasRef: RefObject = + useRef(null); + + const generateStars = useCallback( + (width: number, height: number): StarProps[] => { + const area = width * height; + const numStars = Math.floor(area * starDensity); + return Array.from({ length: numStars }, () => { + const shouldTwinkle = + allStarsTwinkle || Math.random() < twinkleProbability; + return { + x: Math.random() * width, + y: Math.random() * height, + radius: Math.random() * 0.05 + 0.5, + opacity: Math.random() * 0.5 + 0.5, + twinkleSpeed: shouldTwinkle + ? minTwinkleSpeed + + Math.random() * (maxTwinkleSpeed - minTwinkleSpeed) + : null, + }; + }); + }, + [ + starDensity, + allStarsTwinkle, + twinkleProbability, + minTwinkleSpeed, + maxTwinkleSpeed, + ] + ); + + useEffect(() => { + const updateStars = () => { + if (canvasRef.current) { + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const { width, height } = canvas.getBoundingClientRect(); + canvas.width = width; + canvas.height = height; + setStars(generateStars(width, height)); + } + }; + + updateStars(); + + const resizeObserver = new ResizeObserver(updateStars); + if (canvasRef.current) { + resizeObserver.observe(canvasRef.current); + } + + return () => { + if (canvasRef.current) { + resizeObserver.unobserve(canvasRef.current); + } + }; + }, [ + starDensity, + allStarsTwinkle, + twinkleProbability, + minTwinkleSpeed, + maxTwinkleSpeed, + generateStars, + ]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let animationFrameId: number; + + const render = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + stars.forEach((star) => { + ctx.beginPath(); + ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2); + ctx.fillStyle = `rgba(255, 255, 255, ${star.opacity})`; + ctx.fill(); + + if (star.twinkleSpeed !== null) { + star.opacity = + 0.5 + + Math.abs(Math.sin((Date.now() * 0.001) / star.twinkleSpeed) * 0.5); + } + }); + + animationFrameId = requestAnimationFrame(render); + }; + + render(); + + return () => { + cancelAnimationFrame(animationFrameId); + }; + }, [stars]); + + return ( + + ); +}; diff --git a/apps/user-app/app/(dashboard)/bills/page.tsx b/apps/user-app/app/(dashboard)/bills/page.tsx index f706e73..4d8f2e3 100644 --- a/apps/user-app/app/(dashboard)/bills/page.tsx +++ b/apps/user-app/app/(dashboard)/bills/page.tsx @@ -1,125 +1,148 @@ "use client"; - import { useEffect, useState } from "react"; - import { BillPaymentCard } from "@/components/BillPaymentCard"; - import { Button } from "@/components/ui/button"; +import { useEffect, useState } from "react"; +import { BillPaymentCard } from "@/components/BillPaymentCard"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; - type BillSchedule = { - id: number; - userId: number; - merchantId?: number; - billType: string; - provider: string; - accountNo: string; - amount: number; - dueDate: string; - nextPayment?: string; - paymentMethod: string; - status: "PENDING" | "PAID" | "OVERDUE"; - token?: string; - }; +type BillSchedule = { + id: number; + userId: number; + merchantId?: number; + billType: string; + provider: string; + accountNo: string; + amount: number; + dueDate: string; + nextPayment?: string; + paymentMethod: string; + status: "PENDING" | "PAID" | "OVERDUE"; + token?: string; +}; - export default function BillsPage() { - const [schedules, setSchedules] = useState([]); - const [error, setError] = useState(""); - const userId = 1; // Replace with authenticated user ID +export default function BillsPage() { + const [schedules, setSchedules] = useState([]); + const [error, setError] = useState(""); + const userId = 1; // Replace with authenticated user ID - const fetchBills = async () => { - try { - const response = await fetch(`/api/bills?userId=${userId}`); - if (!response.ok) throw new Error(`Failed to fetch bills: ${response.statusText}`); - const data = await response.json(); - setSchedules(data); - setError(""); - } catch (error: any) { - console.error("Error fetching bills:", error); - setError("Failed to load bills. Please try again."); - } - }; + const fetchBills = async () => { + try { + const response = await fetch(`/api/bills?userId=${userId}`); + if (!response.ok) + throw new Error(`Failed to fetch bills: ${response.statusText}`); + const data = await response.json(); + setSchedules(data); + setError(""); + } catch (error: any) { + console.error("Error fetching bills:", error); + setError("Failed to load bills. Please try again."); + } + }; - useEffect(() => { - fetchBills(); - }, []); + useEffect(() => { + fetchBills(); + }, []); - return ( -
-
-

Bill Payments

- -
-
- -
+ return ( +
+

Manage Bills

-
-
-

- Upcoming Bills ({schedules.length}) -

- -
- {error && ( -
- {error} -
- )} -
- {schedules - .sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()) - .map((schedule) => { - const dueDate = new Date(schedule.dueDate); - const isOverdue = dueDate < new Date() && schedule.status !== "PAID"; +
+ - return ( -
-

{schedule.billType}

-

{schedule.provider}

-

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

-

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

-

- Status: {schedule.status} -

-

- Payment Method: {schedule.paymentMethod} -

- {schedule.nextPayment && ( -

- Next Payment: {new Date(schedule.nextPayment).toLocaleDateString("en-IN", { + +

+

+ Upcoming Bills ({schedules.length}) +

+ +
+ {error && ( +
+ {error} +
+ )} +
+
+ {schedules + .sort( + (a, b) => + new Date(a.dueDate).getTime() - + new Date(b.dueDate).getTime() + ) + .map((schedule) => { + const dueDate = new Date(schedule.dueDate); + const isOverdue = + dueDate < new Date() && schedule.status !== "PAID"; + + return ( +
+

+ {schedule.billType} +

+

+ {schedule.provider} +

+

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

+

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

+

+ Status:{" "} + + {schedule.status} + +

+

+ Payment Method: {schedule.paymentMethod} +

+ {schedule.nextPayment && ( +

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

- )} - {isOverdue && ( - - 🚨 Overdue - - )} -
- ); - })} -
- -
- -
-
-
- ); - } \ No newline at end of file + } + )} +

+ )} + {isOverdue && ( + + Overdue + + )} +
+ ); + })} +
+
+ +
+
+ ); +} diff --git a/apps/user-app/app/(dashboard)/dashboard/page.tsx b/apps/user-app/app/(dashboard)/dashboard/page.tsx index d8dcbed..6fac575 100644 --- a/apps/user-app/app/(dashboard)/dashboard/page.tsx +++ b/apps/user-app/app/(dashboard)/dashboard/page.tsx @@ -1,115 +1,155 @@ import { getServerSession } from "next-auth"; import prisma from "@repo/db/client"; import { BalanceCard } from "@/components/BalanceCard"; -import { TransferList } from "@/components/TransferList"; -import { OnRampList } from "@/components/OnRampList"; import { StatsCards } from "@/components/StatsCards"; import { DashboardClient } from "@/components/DashboardClient"; -import { Avatar, AvatarFallback, AvatarImage } from "../../../../../packages/ui/src/avatar"; import { Toaster } from "react-hot-toast"; import { authOptions } from "@/app/lib/auth"; import { startOfDay, subDays, format } from "date-fns"; +import ReturnPendingList from "@/components/ReturnPendingList"; +import { Avatar, AvatarFallback, AvatarImage } from "../../../../../packages/ui/src/avatar"; async function getDashboardData(userId: number) { const balance = await prisma.balance.findFirst({ where: { userId } }); + // 1. P2P Transfers (exclude refunded sent) const transfers = await prisma.p2pTransfer.findMany({ where: { OR: [{ fromUserId: userId }, { toUserId: userId }], + NOT: { status: "REFUNDED", fromUserId: userId }, }, - include: { fromUser: true, toUser: true }, + include: { fromUser: true, toUser: true, wrongSendRequest: true }, orderBy: { timestamp: "desc" }, - take: 5, + take: 10, + }); + + // 2. Pending Returns (receiver side) + const pendingReturns = await prisma.wrongSendRequest.findMany({ + where: { + transaction: { toUserId: userId }, + status: "PENDING", + expiresAt: { gt: new Date() }, + }, + include: { + sender: { select: { name: true } }, + transaction: { select: { amount: true, timestamp: true } }, + }, + orderBy: { expiresAt: "asc" }, }); + // 3. OnRamps const onRamps = await prisma.onRampTransaction.findMany({ where: { userId }, orderBy: { startTime: "desc" }, take: 5, }); - // Aggregate data for chart (last 6 days) - const startDate = startOfDay(subDays(new Date(), 5)); - const endDate = startOfDay(new Date()); - - const sentTransfers = await prisma.p2pTransfer.groupBy({ + // 4. Chart data + const start = startOfDay(subDays(new Date(), 5)); + const sent = await prisma.p2pTransfer.groupBy({ by: ["timestamp"], - where: { fromUserId: userId, timestamp: { gte: startDate, lte: endDate } }, + where: { fromUserId: userId, timestamp: { gte: start } }, _sum: { amount: true }, orderBy: { timestamp: "asc" }, }); - - const receivedTransfers = await prisma.p2pTransfer.groupBy({ + const received = await prisma.p2pTransfer.groupBy({ by: ["timestamp"], - where: { toUserId: userId, timestamp: { gte: startDate, lte: endDate } }, + where: { toUserId: userId, timestamp: { gte: start } }, _sum: { amount: true }, orderBy: { timestamp: "asc" }, }); - const onRampTransactions = await prisma.onRampTransaction.groupBy({ - by: ["startTime"], - where: { userId, startTime: { gte: startDate, lte: endDate } }, - _sum: { amount: true }, - orderBy: { startTime: "asc" }, - }); - - // Generate labels and data for chart - const labels = Array.from({ length: 6 }, (_, i) => format(subDays(new Date(), 5 - i), "yyyy-MM-dd")); - const sentData = labels.map((date) => { - const dayData = sentTransfers.find((t) => format(t.timestamp, "yyyy-MM-dd") === date); - return (dayData?._sum?.amount || 0) / 100; - }); - const receivedData = labels.map((date) => { - const dayData = receivedTransfers.find((t) => format(t.timestamp, "yyyy-MM-dd") === date); - return (dayData?._sum?.amount || 0) / 100; - }); - const onRampData = labels.map((date) => { - const dayData = onRampTransactions.find((t) => format(t.startTime, "yyyy-MM-dd") === date); - return (dayData?._sum?.amount || 0) / 100; - }); + const labels = Array.from({ length: 6 }, (_, i) => format(subDays(new Date(), 5 - i), "MMM dd")); + const sentData = labels.map(d => (sent.find(t => format(t.timestamp, "MMM dd") === d)?._sum?.amount || 0) / 100); + const receivedData = labels.map(d => (received.find(t => format(t.timestamp, "MMM dd") === d)?._sum?.amount || 0) / 100); return { balance: { amount: balance?.amount || 0, locked: balance?.locked || 0 }, transfers, + pendingReturns: pendingReturns.map(r => ({ + id: r.id, + senderName: r.sender.name ?? "Unknown", + amount: Number(r.transaction.amount) / 100, + expiresAt: r.expiresAt, + timestamp: r.transaction.timestamp, + })), onRamps, - chartData: { labels, sentData, receivedData, onRampData }, + chartData: { labels, sentData, receivedData }, }; } export default async function DashboardPage() { const session = await getServerSession(authOptions); if (!session?.user?.id) { - return
Please log in to view your dashboard.
; + return
Please log in.
; } const userId = Number(session.user.id); - const { balance, transfers, onRamps } = await getDashboardData(userId); + const { balance, transfers, pendingReturns, onRamps } = await getDashboardData(userId); return ( -
- {/* User Greeting */} -
+
+ {/* Greeting */} +
- - {session.user.name?.[0] || "U"} + + {session.user.name?.[0] ?? "U"} -

- Welcome, {session.user.name || "User"} +

+ Hi, {session.user.name ?? "User"}

+ {/* Pending Returns */} + {pendingReturns.length > 0 && ( +
+

+ Pending Returns {`(${pendingReturns.length})`} +

+
+ +
+
+ )} - {/* Top Section */} + {/* Balance + Stats */}
- {/* Transfers & OnRamps */} -
- - -
+ + + {/* Recent Activity */} +
+

Recent Activity

+
+ {transfers.map((t) => { + const isSent = t.fromUserId === userId; + return ( +
+
+ +
+

+ {isSent ? `Sent to ${t.toUser?.name ?? "Unknown"}` : `From ${t.fromUser?.name ?? "Unknown"}`} +

+

+ {format(new Date(t.timestamp), "MMM dd, h:mm a")} +

+
+
+

+ {isSent ? "-" : "+"}₹{(Number(t.amount) / 100).toFixed(0)} +

+
+ ); + })} +
+
diff --git a/apps/user-app/app/(dashboard)/layout.tsx b/apps/user-app/app/(dashboard)/layout.tsx index db8ba28..0d4dcbc 100644 --- a/apps/user-app/app/(dashboard)/layout.tsx +++ b/apps/user-app/app/(dashboard)/layout.tsx @@ -1,73 +1,61 @@ "use client"; import { useSession } from "next-auth/react"; -import { BrickWallFire, Home, LayoutDashboard, Repeat, Send,} from 'lucide-react'; -import { - Avatar, - AvatarFallback, - AvatarImage, -} from '../../../../packages/ui/src/avatar'; -import { TooltipProvider, } from '../../../../packages/ui/src/tooltip'; +import { Home, LayoutDashboard, Send, Repeat, BrickWallFire, Wallet } from 'lucide-react'; +import { Avatar, AvatarFallback, AvatarImage } from '../../../../packages/ui/src/avatar'; +import { TooltipProvider } from '../../../../packages/ui/src/tooltip'; import { Sidebar, SidebarBody, SidebarLink, SignupBtn } from '../../../../packages/ui/src/sidebar'; +import { useMemo } from 'react'; - -// Your Links Interface interface Links { label: string; href: string; - icon: React.JSX.Element | React.ReactNode; + icon: React.JSX.Element; } -const sidebarLinks: Links[] = [ - { label: "Home", href: "/", icon: }, - { label: "Dashboard", href: "/dashboard", icon: }, - { label: "Transfer", href: "/transfer", icon: }, - { label: "P2P Transfer", href: "/p2p", icon: }, - { label: "Bills", href: "/bills", icon: }, -]; - - -export default function Layout({ - children, -}: { - children: React.ReactNode; -}): JSX.Element { +export default function Layout({ children }: { children: React.ReactNode }) { const { data: session } = useSession(); + const sidebarLinks = useMemo(() => [ + { label: "Home", href: "/", icon: }, + { label: "Dashboard", href: "/dashboard", icon: }, + { label: "Transfer", href: "/transfer", icon: }, + { label: "P2P Transfer", href: "/p2p", icon: }, + { label: "Bills", href: "/bills", icon: }, + { label: "Recharge", href: "/recharge", icon: }, + ], []); + return (
- -