Implement new features and enhance existing components for improved user experience

- Added a new `bun.lockb` file for dependency management.
- Updated `next.config.ts` to set output to "standalone" for better deployment options.
- Removed `package-lock.json` to streamline package management.
- Modified `package.json` to update dependencies, including `@prisma/client` and `sonner`, and adjusted build scripts for improved functionality.
- Enhanced Tailwind CSS configuration to include new animations and color schemes.
- Refactored various dashboard components to improve UI consistency, including adding a new `My Wallet` page and updating existing pages to use a unified styling approach.
- Introduced a new `BlockDeviceDialog` component for managing device blocking with user-defined reasons.
- Improved logging and error handling in payment verification and device management functions.

These changes enhance the overall functionality, maintainability, and user experience of the application.
This commit is contained in:
i701 2024-12-26 20:25:38 +05:00
parent 5fb6f52bfc
commit bdf3729b0d
25 changed files with 299 additions and 10198 deletions

View File

@ -56,7 +56,7 @@ export async function addDevicesToGroup({
try { try {
// Fetch the existing group profiles // Fetch the existing group profiles
const groupProfiles: OmadaResponse = await fetchOmadaGroupProfiles(siteId); const groupProfiles: OmadaResponse = await fetchOmadaGroupProfiles(siteId);
console.log(groupProfiles); // console.log(groupProfiles);
// Find the group profile with the specified groupId // Find the group profile with the specified groupId
const groupProfile: GroupProfile | undefined = const groupProfile: GroupProfile | undefined =
groupProfiles.result.data.find((profile) => profile.groupId === groupId); groupProfiles.result.data.find((profile) => profile.groupId === groupId);
@ -70,7 +70,7 @@ export async function addDevicesToGroup({
...(groupProfile.macAddressList || []), ...(groupProfile.macAddressList || []),
...newDevices, ...newDevices,
]; ];
console.log({ updatedMacAddressList }); // console.log({ updatedMacAddressList });
// Prepare the request payload // Prepare the request payload
const requestBody = { const requestBody = {
name: groupProfile.name, name: groupProfile.name,
@ -112,7 +112,8 @@ export async function addDevicesToGroup({
export async function blockDevice({ export async function blockDevice({
macAddress, macAddress,
type, type,
}: { macAddress: string; type: "block" | "unblock" }) { reason
}: { macAddress: string; type: "block" | "unblock", reason?: string }) {
console.log("hello world asdasd"); console.log("hello world asdasd");
if (!macAddress) { if (!macAddress) {
throw new Error("macAddress is a required parameter"); throw new Error("macAddress is a required parameter");
@ -144,6 +145,7 @@ export async function blockDevice({
id: device?.id, id: device?.id,
}, },
data: { data: {
reasonForBlocking: type === "block" ? reason : "",
blocked: type === "block", blocked: type === "block",
}, },
}); });

View File

@ -107,6 +107,7 @@ async function verifyExternalPayment(
data: VerifyPaymentType, data: VerifyPaymentType,
payment: PaymentWithDevices | null, payment: PaymentWithDevices | null,
): Promise<VerifyPaymentResponse> { ): Promise<VerifyPaymentResponse> {
console.log('payment verify data ->', data)
const response = await fetch( const response = await fetch(
"https://verifypaymentsapi.baraveli.dev/verify-payment", "https://verifypaymentsapi.baraveli.dev/verify-payment",
{ {
@ -117,7 +118,7 @@ async function verifyExternalPayment(
); );
const json = await response.json(); const json = await response.json();
console.log(json)
if (!payment) { if (!payment) {
throw new Error("Payment verification failed or payment not found"); throw new Error("Payment verification failed or payment not found");
} }

View File

@ -2,6 +2,12 @@ import React from 'react'
export default function Agreements() { export default function Agreements() {
return ( return (
<div>Agreements</div> <div>
<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">
Agreements
</h3>
</div>
</div>
) )
} }

View File

@ -21,8 +21,8 @@ export default async function Devices({
const user = await getCurrentUser() const user = await getCurrentUser()
return ( return (
<div> <div>
<div className="flex justify-between items-center text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3> <h3 className="text-sarLinkOrange text-2xl">
My Devices My Devices
</h3> </h3>
<AddDeviceDialogForm user_id={user?.id} /> <AddDeviceDialogForm user_id={user?.id} />

View File

@ -18,8 +18,8 @@ export default async function ParentalControl({
const query = (await searchParams)?.query || ""; const query = (await searchParams)?.query || "";
return ( return (
<div> <div>
<div className="flex justify-between items-center text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3> <h3 className="text-sarLinkOrange text-2xl">
Parental Control Parental Control
</h3> </h3>
</div> </div>

View File

@ -26,12 +26,13 @@ export default async function PaymentPage({
devices: true, devices: true,
}, },
}); });
const formula = await prisma.billFormula.findFirst();
return ( return (
<div> <div>
<div className="flex justify-between items-center text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3>Payment</h3> <h3 className="text-sarLinkOrange text-2xl">
<span className={cn("text-sm border px-4 py-2 rounded-md uppercase font-semibold", payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")}> Payment
</h3>
<span className={cn("text-sm border px-4 py-2 rounded-md uppercase font-semibold", payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-700")}>
{payment?.paid ? "Paid" : "Pending"} {payment?.paid ? "Paid" : "Pending"}
</span> </span>
</div> </div>
@ -42,7 +43,6 @@ export default async function PaymentPage({
> >
<DevicesToPay <DevicesToPay
user={user || undefined} user={user || undefined}
billFormula={formula ?? undefined}
payment={payment || undefined} payment={payment || undefined}
/> />
</div> </div>

View File

@ -15,8 +15,10 @@ export default async function Devices({
const query = (await searchParams)?.query || ""; const query = (await searchParams)?.query || "";
return ( return (
<div> <div>
<div className="flex justify-between items-center text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3>My Payments</h3> <h3 className="text-sarLinkOrange text-2xl">
My Payments
</h3>
</div> </div>
<div <div

View File

@ -1,9 +1,11 @@
export default async function UserDevcies() { export default async function UserDevcies() {
return ( return (
<div> <div>
<h3 className="text-2xl font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
User Devices <h3 className="text-sarLinkOrange text-2xl">
</h3> User Devices
</h3>
</div>
</div> </div>
); );
} }

View File

@ -5,7 +5,11 @@ export default async function UserPayments() {
await AdminAuthGuard(); await AdminAuthGuard();
return ( return (
<div> <div>
<h3>User Payments</h3> <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">
User Payments
</h3>
</div>
</div> </div>
); );
} }

View File

@ -38,9 +38,11 @@ export default async function AdminUsers({
return ( return (
<div> <div>
<h3 className="text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
Users <h3 className="text-sarLinkOrange text-2xl">
</h3> Users
</h3>
</div>
<div <div
id="user-table-filters" id="user-table-filters"

View File

@ -0,0 +1,13 @@
import React from 'react'
export default function UserWallet() {
return (
<div>
<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">
My Wallet
</h3>
</div>
</div>
)
}

File diff suppressed because one or more lines are too long

BIN
bun.lockb Executable file

Binary file not shown.

View File

@ -30,6 +30,7 @@ export async function ApplicationLayout({
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar role={session?.user?.role || "USER"} /> <AppSidebar role={session?.user?.role || "USER"} />
<DeviceCartDrawer billFormula={billFormula || null} />
<SidebarInset> <SidebarInset>
<header className="flex justify-between sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b px-4 z-10"> <header className="flex justify-between sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b px-4 z-10">
<div className="flex items-center gap-2 "> <div className="flex items-center gap-2 ">
@ -39,7 +40,6 @@ export async function ApplicationLayout({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Wallet walletBalance={user?.walletBalance || 0} /> <Wallet walletBalance={user?.walletBalance || 0} />
<DeviceCartDrawer billFormula={billFormula || null} />
<ModeToggle /> <ModeToggle />
<AccountPopover /> <AccountPopover />
</div> </div>

View File

@ -1,35 +1,135 @@
'use client' "use client"
import { blockDevice } from "@/actions/omada-actions"; import { blockDevice } from "@/actions/omada-actions"
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import type { Device } from "@prisma/client"; import {
import { useState } from "react"; Dialog,
import { toast } from "sonner"; DialogContent,
import { TextShimmer } from "./ui/text-shimmer"; DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils"
import { zodResolver } from "@hookform/resolvers/zod"
import type { Device, } from "@prisma/client"
import { OctagonX } from "lucide-react"
import { useState } from "react"
import { type SubmitHandler, useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { Textarea } from "./ui/textarea"
import { TextShimmer } from "./ui/text-shimmer"
const validationSchema = z.object({
reasonForBlocking: z.string().min(5, { message: "Reason is required" }),
})
export default function BlockDeviceDialog({ device, type }: { device: Device, type: "block" | "unblock" }) {
const [disabled, setDisabled] = useState(false)
const [open, setOpen] = useState(false)
const {
register,
handleSubmit,
formState: { errors },
} = useForm<z.infer<typeof validationSchema>>({
resolver: zodResolver(validationSchema),
})
const onSubmit: SubmitHandler<z.infer<typeof validationSchema>> = (data) => {
setDisabled(true)
console.log(data)
toast.promise(blockDevice({
macAddress: device.mac,
type: type,
reason: data.reasonForBlocking,
// reason: data.reasonForBlocking,
}), {
loading: "Blocking...",
success: () => {
setDisabled(false)
setOpen((prev) => !prev)
return "Blocked!"
},
error: (error) => {
setDisabled(false)
return error || "Something went wrong"
},
})
setDisabled(false)
}
export default function BlockDeviceDialog({ device }: { device: Device }) {
const [disabled, setDisabled] = useState(false);
return ( return (
<Button <div>
className="w-full mt-2" {device.blocked ? (
disabled={disabled} <Button onClick={
onClick={() => { () => {
setDisabled(true); setDisabled(true);
toast.promise(blockDevice({ macAddress: device.mac, type: device.blocked ? "unblock" : "block" }), { toast.promise(blockDevice({
loading: device.blocked ? "Unblocking..." : "Blocking...", macAddress: device.mac,
success: () => { type: "unblock",
setDisabled(false); reason: '',
return `Device ${device.name} successfully ${device.blocked ? "unblocked" : "blocked" }), {
}!`; loading: "unblockinig...",
}, success: () => {
error: () => { setDisabled(false);
setDisabled(false); return "Unblocked!";
return "Something went wrong"; },
}, error: () => {
}); setDisabled(false);
}} return "Something went wrong";
> },
{disabled ? <TextShimmer>{device.blocked ? "Unblocking..." : "Blocking..."}</TextShimmer> : (device?.blocked ? "Unblock" : "Block")} })
</Button> }
}>
{disabled ? (
<TextShimmer>
Unblocking
</TextShimmer>
) : "Unblock"}
</Button>
) : (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button disabled={disabled} variant="destructive">
<OctagonX />
Block
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Please provide a reason for blocking this device.</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-4 py-4">
<div className="flex flex-col items-start gap-1">
<Label htmlFor="reason" className="text-right">
Reason for blocking
</Label>
<Textarea rows={10} {...register("reasonForBlocking")} id="reasonForBlocking" className={cn("col-span-5", errors.reasonForBlocking && "ring-2 ring-red-500")} />
<span className="text-sm text-red-500">
{errors.reasonForBlocking?.message}
</span>
</div>
</div>
<DialogFooter>
<Button variant={"destructive"} disabled={disabled} type="submit">
Block
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)}
</div>
) )
} }

View File

@ -45,11 +45,11 @@ export default function ClickableRow({ device, parentalControl }: { device: Devi
year: "numeric", year: "numeric",
})} })}
</span> </span>
{parentalControl && ( {(parentalControl && device.blocked) && (
<div className="p-2 rounded border my-2"> <div className="p-2 rounded border my-2">
<span>Comment: </span> <span>Comment: </span>
<p className="text-neutral-500"> <p className="text-neutral-500">
blocked because he was watching youtube {device?.reasonForBlocking}
</p> </p>
</div> </div>
)} )}
@ -60,7 +60,7 @@ export default function ClickableRow({ device, parentalControl }: { device: Devi
{!parentalControl ? ( {!parentalControl ? (
<AddDevicesToCartButton device={device} /> <AddDevicesToCartButton device={device} />
) : ( ) : (
<BlockDeviceDialog device={device} /> <BlockDeviceDialog type={device.blocked ? "unblock" : "block"} device={device} />
)} )}
</TableCell> </TableCell>
</TableRow > </TableRow >

View File

@ -55,7 +55,7 @@ export default function DeviceCard({ device, parentalControl }: { device: Device
{device.blocked && ( {device.blocked && (
<div className="p-2 rounded border my-2 w-full"> <div className="p-2 rounded border my-2 w-full">
<span>Comment: </span> <span className='uppercase text-red-500'>Blocked by admin </span>
<p className="text-neutral-500"> <p className="text-neutral-500">
blocked because he was watching youtube blocked because he was watching youtube
</p> </p>
@ -67,7 +67,7 @@ export default function DeviceCard({ device, parentalControl }: { device: Device
{!parentalControl ? ( {!parentalControl ? (
<AddDevicesToCartButton device={device} /> <AddDevicesToCartButton device={device} />
) : ( ) : (
<BlockDeviceDialog device={device} /> <BlockDeviceDialog type={device.blocked ? "unblock" : "block"} device={device} />
)} )}
</div> </div>
</div> </div>

View File

@ -33,7 +33,6 @@ import { toast } from "sonner";
import NumberInput from "./number-input"; import NumberInput from "./number-input";
export function DeviceCartDrawer({ export function DeviceCartDrawer({
billFormula, billFormula,
}: { }: {
@ -74,83 +73,88 @@ export function DeviceCartDrawer({
paid: false, paid: false,
}; };
if (devices.length === 0) return null
return ( return (
<Drawer open={isOpen} onOpenChange={setIsOpen}> <>
<DrawerTrigger asChild> <Drawer open={isOpen} onOpenChange={setIsOpen}>
<Button onClick={() => setIsOpen(!isOpen)} variant="outline"> <DrawerTrigger asChild>
<MonitorSmartphone /> <Button size={"lg"} className="bg-sarLinkOrange absolute bottom-10 w-fit z-20 left-1/2 transform -translate-x-1/2" onClick={() => setIsOpen(!isOpen)} variant="outline">
{devices.length > 0 && `(${devices.length})`} <MonitorSmartphone />
</Button> Pay {devices.length > 0 && `(${devices.length})`} Device
</DrawerTrigger> </Button>
<DrawerContent> </DrawerTrigger>
<div className="mx-auto w-full max-w-sm"> <DrawerContent>
<DrawerHeader> <div className="mx-auto w-full max-w-sm">
<DrawerTitle>Selected Devices</DrawerTitle> <DrawerHeader>
<DrawerDescription>Selected devices pay.</DrawerDescription> <DrawerTitle>Selected Devices</DrawerTitle>
</DrawerHeader> <DrawerDescription>Selected devices pay.</DrawerDescription>
<div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto px-4 pb-4 gap-4"> </DrawerHeader>
{devices.map((device) => ( <div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto px-4 pb-4 gap-4">
<DeviceCard key={device.id} device={device} /> {devices.map((device) => (
))} <DeviceCard key={device.id} device={device} />
</div> ))}
<div className="px-4 flex flex-col gap-4"> </div>
<NumberInput <div className="px-4 flex flex-col gap-4">
label="Set No of Months" <NumberInput
value={months} label="Set No of Months"
onChange={(value) => setMonths(value)} value={months}
maxAllowed={12} onChange={(value) => setMonths(value)}
isDisabled={devices.length === 0} maxAllowed={12}
/> isDisabled={devices.length === 0}
{message && ( />
<span className="title-bg text-lime-800 bg-lime-100/50 dark:text-lime-100 rounded text-center p-2 w-full"> {message && (
{message} <span className="title-bg text-lime-800 bg-lime-100/50 dark:text-lime-100 rounded text-center p-2 w-full">
</span> {message}
)} </span>
</div>
<DrawerFooter>
<Button
onClick={async () => {
setDisabled(true)
const payment = await createPayment(data)
setDisabled(false)
setDeviceCart([])
setMonths(1)
if (payment) {
router.push(`/payments/${payment.id}`);
setTimeout(() => setIsOpen(!isOpen), 2000);
} else {
toast.error("Something went wrong.")
}
}}
className="w-full"
disabled={devices.length === 0 || disabled}
>
{disabled ? (
<>
<Loader2 className="ml-2 animate-spin" />
</>
) : (
<>
Go to payment
<CircleDollarSign />
</>
)} )}
</Button> </div>
<DrawerClose asChild> <DrawerFooter>
<Button variant="outline">Cancel</Button> <Button
</DrawerClose> onClick={async () => {
<Button setDisabled(true)
onClick={() => { const payment = await createPayment(data)
setDeviceCart([]); setDisabled(false)
}} setDeviceCart([])
variant="outline" setMonths(1)
> if (payment) {
Reset router.push(`/payments/${payment.id}`);
</Button> setTimeout(() => setIsOpen(!isOpen), 2000);
</DrawerFooter> } else {
</div> toast.error("Something went wrong.")
</DrawerContent> }
</Drawer> }}
className="w-full"
disabled={devices.length === 0 || disabled}
>
{disabled ? (
<>
<Loader2 className="ml-2 animate-spin" />
</>
) : (
<>
Go to payment
<CircleDollarSign />
</>
)}
</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
<Button
onClick={() => {
setDeviceCart([]);
setIsOpen(!isOpen);
}}
variant="outline"
>
Clear Selection
</Button>
</DrawerFooter>
</div>
</DrawerContent>
</Drawer>
</>
); );
} }

View File

@ -9,7 +9,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { formatDate } from "@/lib/utils"; import { formatDate } from "@/lib/utils";
import type { BillFormula, Prisma, User } from "@prisma/client"; import type { Prisma, User } from "@prisma/client";
import { BadgeDollarSign, Clipboard, ClipboardCheck, Loader2, Wallet } from "lucide-react"; import { BadgeDollarSign, Clipboard, ClipboardCheck, Loader2, Wallet } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@ -22,28 +22,24 @@ type PaymentWithDevices = Prisma.PaymentGetPayload<{
}>; }>;
export default function DevicesToPay({ export default function DevicesToPay({
billFormula,
payment, payment,
user user
}: { billFormula?: BillFormula; payment?: PaymentWithDevices, user?: User }) { }: { payment?: PaymentWithDevices, user?: User }) {
const [verifying, setVerifying] = useState(false) const [verifying, setVerifying] = useState(false)
const devices = payment?.devices; const devices = payment?.devices;
if (devices?.length === 0) { if (devices?.length === 0) {
return null; return null;
} }
const baseAmount = billFormula?.baseAmount ?? 100;
const discountPercentage = billFormula?.discountPercentage ?? 75;
// 100+(n1)×75 // 100+(n1)×75
const total = baseAmount + (devices?.length ?? 1 - 1) * discountPercentage;
const walletBalance = user?.walletBalance ?? 0; const walletBalance = user?.walletBalance ?? 0;
const isWalletPayVisible = walletBalance > total; const isWalletPayVisible = walletBalance > (payment?.amount ?? 0);
return ( return (
<div className="w-full"> <div className="w-full">
<div className="p-2 flex flex-col gap-2"> <div className="p-2 flex flex-col gap-2">
<h3 className="title-bg my-1 p-2 font-semibold text-lg"> <h3 className="title-bg my-1 p-2 border border-dashed rounded-md font-semibold text-lg">
{!payment?.paid ? "Devices to pay" : "Devices Paid"} {!payment?.paid ? "Devices to pay" : "Devices Paid"}
</h3> </h3>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -85,7 +81,7 @@ export default function DevicesToPay({
paymentId: payment?.id, paymentId: payment?.id,
benefName: user?.name ?? "", benefName: user?.name ?? "",
accountNo: user?.accNo ?? "", accountNo: user?.accNo ?? "",
absAmount: String(total), absAmount: String(payment?.amount),
time: formatDate(new Date(payment?.createdAt || "")), time: formatDate(new Date(payment?.createdAt || "")),
type: "WALLET", type: "WALLET",
}); });
@ -105,7 +101,7 @@ export default function DevicesToPay({
paymentId: payment?.id, paymentId: payment?.id,
benefName: user?.name ?? "", benefName: user?.name ?? "",
accountNo: user?.accNo ?? "", accountNo: user?.accNo ?? "",
absAmount: String(total), absAmount: String(payment?.amount),
type: "TRANSFER", type: "TRANSFER",
time: formatDate(new Date(payment?.createdAt || "")), time: formatDate(new Date(payment?.createdAt || "")),
}); });
@ -140,7 +136,7 @@ export default function DevicesToPay({
<TableFooter> <TableFooter>
<TableRow className=""> <TableRow className="">
<TableCell colSpan={1}>Total Due</TableCell> <TableCell colSpan={1}>Total Due</TableCell>
<TableCell className="text-right text-3xl font-bold">{total.toFixed(2)}</TableCell> <TableCell className="text-right text-3xl font-bold">{payment?.amount.toFixed(2)}</TableCell>
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>

View File

@ -104,7 +104,7 @@ export async function PaymentsTable({
{payments.map((payment) => ( {payments.map((payment) => (
<TableRow key={payment.id}> <TableRow key={payment.id}>
<TableCell> <TableCell>
<div className={cn("flex flex-col items-start title-bg border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}> <div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar size={16} opacity={0.5} /> <Calendar size={16} opacity={0.5} />
<span className="text-muted-foreground"> <span className="text-muted-foreground">

View File

@ -35,8 +35,10 @@ export default function PriceCalculator() {
return ( return (
<div className="border p-2 rounded-xl"> <div className="border p-2 rounded-xl">
<div className="flex flex-col justify-between items-start text-gray-500 title-bg p-2 mb-4"> <div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-2xl font-semibold">Price Calculator</h3> <h3 className="text-sarLinkOrange text-2xl">
Price Calculator
</h3>
</div> </div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{/* Initial Price Input */} {/* Initial Price Input */}

View File

@ -2,6 +2,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
output: "standalone",
}; };
export default nextConfig; export default nextConfig;

10036
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "bunx prisma migrate deploy && bunx prisma generate && bunx prisma db push && next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint"
}, },
@ -14,7 +14,7 @@
"dependencies": { "dependencies": {
"@faker-js/faker": "^9.3.0", "@faker-js/faker": "^9.3.0",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@prisma/client": "^5.22.0", "@prisma/client": "^6.1.0",
"@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.1",
@ -33,33 +33,33 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"jotai": "^2.8.0", "jotai": "2.8.0",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
"motion": "^11.15.0", "motion": "^11.15.0",
"next": "15.1.2", "next": "15.1.2",
"next-themes": "^0.4.3", "next-themes": "^0.4.3",
"nextjs-toploader": "^3.7.15", "nextjs-toploader": "^3.7.15",
"prisma": "^5.22.0", "prisma": "^6.1.0",
"react": "19.0.0", "react": "19.0.0",
"react-aria-components": "^1.5.0", "react-aria-components": "^1.5.0",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-hook-form": "^7.53.2", "react-hook-form": "^7.53.2",
"react-phone-number-input": "^3.4.9", "react-phone-number-input": "^3.4.9",
"sonner": "^1.7.0", "sonner": "^1.7.1",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.1", "vaul": "^1.1.2",
"zod": "^3.23.8" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/react": "^19.0.2", "@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2", "@types/react-dom": "^19.0.2",
"eslint": "^8", "eslint": "^9.17.0",
"eslint-config-next": "15.1.2", "eslint-config-next": "15.1.2",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.17",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.7.2" "typescript": "^5.7.2"
} }

View File

@ -1,4 +1,5 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
export default { export default {
darkMode: ["class"], darkMode: ["class"],
@ -10,6 +11,7 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
sarLinkOrange: "#f49b5b",
background: "hsl(var(--background))", background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))", foreground: "hsl(var(--foreground))",
card: { card: {
@ -72,5 +74,5 @@ export default {
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [tailwindcssAnimate],
} satisfies Config; } satisfies Config;