mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-07-01 15:23:58 +00:00
Refactor authentication middleware to use native fetch, update dependencies, and enhance error handling. Add new error boundary component for dashboard and improve user verification UI. Update payment handling and device management components for better user experience. Adjust CSS for error backgrounds and refine input read-only components with validation indicators.
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 3m9s
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 3m9s
This commit is contained in:
@ -151,7 +151,7 @@ export async function AdminDevicesTable({
|
||||
{device.blocked ? "Blocked" : "Not Blocked"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{device.blockedBy ? device.blockedBy : "Not Blocked"}
|
||||
{device.blocked ? device.blockedBy : ""}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date().toLocaleDateString("en-US", {
|
||||
|
@ -49,7 +49,7 @@ export async function ApplicationLayout({
|
||||
<AccountPopover />
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-4 ">{children}</div>
|
||||
<div className="p-4 flex flex-col flex-1">{children}</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
@ -48,7 +48,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
if (actionState.db_error === "invalidPersonValidation") {
|
||||
return (
|
||||
<>
|
||||
<div className="h-24 w-72 text-center text-green-500 p-4 flex my-4 flex-col items-center justify-center border dark:title-bg bg-white rounded-lg">{actionState.message}</div>
|
||||
<div className="h-24 w-72 text-center text-green-500 p-4 flex my-4 flex-col items-center justify-center border dark:title-bg bg-white dark:bg-black rounded-lg">{actionState.message}</div>
|
||||
<div className="mb-4 text-center text-sm">
|
||||
Go to {" "}
|
||||
<Link href="login" className="underline">
|
||||
|
@ -1,200 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { createPayment } from "@/actions/payment";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import {
|
||||
cartDrawerOpenAtom,
|
||||
deviceCartAtom,
|
||||
numberOfMonths,
|
||||
deviceCartAtom
|
||||
} from "@/lib/atoms";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import type { PaymentType } from "@/lib/types";
|
||||
import type { BillFormula, Device } from "@prisma/client";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
CircleDollarSign,
|
||||
Loader2,
|
||||
MonitorSmartphone,
|
||||
Trash2,
|
||||
MonitorSmartphone
|
||||
} from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import NumberInput from "./number-input";
|
||||
|
||||
|
||||
export function DeviceCartDrawer({
|
||||
billFormula,
|
||||
}: {
|
||||
billFormula: BillFormula | null;
|
||||
}) {
|
||||
const baseAmount = billFormula?.baseAmount || 100;
|
||||
const discountPercentage = billFormula?.discountPercentage || 75;
|
||||
const session = authClient.useSession();
|
||||
export function DeviceCartDrawer() {
|
||||
const pathname = usePathname();
|
||||
const devices = useAtomValue(deviceCartAtom);
|
||||
const setDeviceCart = useSetAtom(deviceCartAtom);
|
||||
const [months, setMonths] = useAtom(numberOfMonths);
|
||||
const [isOpen, setIsOpen] = useAtom(cartDrawerOpenAtom);
|
||||
const [message, setMessage] = useState("");
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (months === 7) {
|
||||
setMessage("You will get 1 month free.");
|
||||
} else if (months === 12) {
|
||||
setMessage("You will get 2 months free.");
|
||||
} else {
|
||||
setMessage("");
|
||||
}
|
||||
setTotal(baseAmount + ((devices.length + 1) - 1) * discountPercentage);
|
||||
}, [months, devices.length, baseAmount, discountPercentage]);
|
||||
|
||||
if (pathname === "/payment") {
|
||||
|
||||
if (pathname === "/payment" || pathname === "/devices-to-pay") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data: PaymentType = {
|
||||
numberOfMonths: months,
|
||||
userId: session?.data?.user.id ?? "",
|
||||
deviceIds: devices.map((device) => device.id),
|
||||
amount: Number.parseFloat(total.toFixed(2)),
|
||||
paid: false,
|
||||
};
|
||||
|
||||
|
||||
if (devices.length === 0) return null
|
||||
return (
|
||||
<>
|
||||
<Drawer open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<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">
|
||||
<MonitorSmartphone />
|
||||
Pay {devices.length > 0 && `(${devices.length})`} Device
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<div className="mx-auto w-full max-w-sm">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Selected Devices</DrawerTitle>
|
||||
<DrawerDescription>Selected devices pay.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto px-4 pb-4 gap-4">
|
||||
<pre>{JSON.stringify(isOpen, null, 2)}</pre>
|
||||
{devices.map((device) => (
|
||||
<DeviceCard key={device.id} device={device} />
|
||||
))}
|
||||
</div>
|
||||
<div className="px-4 flex flex-col gap-4">
|
||||
<NumberInput
|
||||
label="Set No of Months"
|
||||
value={months}
|
||||
onChange={(value) => setMonths(value)}
|
||||
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}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setDisabled(true);
|
||||
toast.promise(
|
||||
createPayment(data).then((result) => {
|
||||
if (result.success) {
|
||||
setDeviceCart([]);
|
||||
setMonths(1);
|
||||
setDisabled(false);
|
||||
if (isOpen) router.push(`/payments/${result.paymentId}`);
|
||||
setIsOpen(!isOpen);
|
||||
return "Payment created!";
|
||||
}
|
||||
}),
|
||||
{
|
||||
loading: "Processing payment...",
|
||||
success: "Payment created!",
|
||||
error: (err) => err.message || "Something went wrong.",
|
||||
}
|
||||
);
|
||||
}}
|
||||
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>
|
||||
</>
|
||||
return <Button size={"lg"} className="bg-sarLinkOrange absolute bottom-10 w-fit z-20 left-1/2 transform -translate-x-1/2" onClick={() => router.push("/devices-to-pay")} variant="outline">
|
||||
<MonitorSmartphone />
|
||||
Pay {devices.length > 0 && `(${devices.length})`} Device
|
||||
</Button>
|
||||
|
||||
);
|
||||
// <>
|
||||
// <Drawer open={isOpen} onOpenChange={setIsOpen}>
|
||||
// <DrawerTrigger asChild>
|
||||
// <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">
|
||||
// <MonitorSmartphone />
|
||||
// Pay {devices.length > 0 && `(${devices.length})`} Device
|
||||
// </Button>
|
||||
// </DrawerTrigger>
|
||||
// <DrawerContent>
|
||||
// <div className="mx-auto w-full max-w-sm">
|
||||
// <DrawerHeader>
|
||||
// <DrawerTitle>Selected Devices</DrawerTitle>
|
||||
// <DrawerDescription>Selected devices pay.</DrawerDescription>
|
||||
// </DrawerHeader>
|
||||
// <div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto px-4 pb-4 gap-4">
|
||||
// <pre>{JSON.stringify(isOpen, null, 2)}</pre>
|
||||
// {devices.map((device) => (
|
||||
// <DeviceCard key={device.id} device={device} />
|
||||
// ))}
|
||||
// </div>
|
||||
// <div className="px-4 flex flex-col gap-4">
|
||||
// <NumberInput
|
||||
// label="Set No of Months"
|
||||
// value={months}
|
||||
// onChange={(value) => setMonths(value)}
|
||||
// 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}
|
||||
// </span>
|
||||
// )}
|
||||
// </div>
|
||||
// <DrawerFooter>
|
||||
// <Button
|
||||
// onClick={async () => {
|
||||
// setDisabled(true);
|
||||
// toast.promise(
|
||||
// createPayment(data).then((result) => {
|
||||
// if (result.success) {
|
||||
// setDeviceCart([]);
|
||||
// setMonths(1);
|
||||
// setDisabled(false);
|
||||
// if (isOpen) router.push(`/payments/${result.paymentId}`);
|
||||
// setIsOpen(!isOpen);
|
||||
// return "Payment created!";
|
||||
// }
|
||||
// }),
|
||||
// {
|
||||
// loading: "Processing payment...",
|
||||
// success: "Payment created!",
|
||||
// error: (err) => err.message || "Something went wrong.",
|
||||
// }
|
||||
// );
|
||||
// }}
|
||||
// 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>
|
||||
// </>
|
||||
|
||||
// );
|
||||
}
|
||||
|
||||
function DeviceCard({ device }: { device: Device }) {
|
||||
const setDeviceCart = useSetAtom(deviceCartAtom);
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center pr-4 justify-between rounded-lg border border-input bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="input-33"
|
||||
className="block px-3 pt-2 text-xs font-medium text-foreground"
|
||||
>
|
||||
{device.name}
|
||||
</label>
|
||||
<input
|
||||
className="flex h-10 opacity-50 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
|
||||
value={device.mac}
|
||||
readOnly
|
||||
disabled
|
||||
placeholder={"MAC Address"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDeviceCart((prev) => prev.filter((d) => d.id !== device.id));
|
||||
}}
|
||||
variant={"destructive"}
|
||||
>
|
||||
Remove
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
107
components/devices-for-payment.tsx
Normal file
107
components/devices-for-payment.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { createPayment } from "@/actions/payment";
|
||||
import DeviceCard from "@/components/device-card";
|
||||
import NumberInput from "@/components/number-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
deviceCartAtom,
|
||||
numberOfMonths
|
||||
} from "@/lib/atoms";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import type { PaymentType } from "@/lib/types";
|
||||
import type { BillFormula } from "@prisma/client";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import {
|
||||
CircleDollarSign,
|
||||
Loader2
|
||||
} from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
export default function DevicesForPayment({
|
||||
billFormula,
|
||||
}: {
|
||||
billFormula?: BillFormula;
|
||||
}) {
|
||||
const baseAmount = billFormula?.baseAmount || 100;
|
||||
const discountPercentage = billFormula?.discountPercentage || 75;
|
||||
const session = authClient.useSession();
|
||||
const pathname = usePathname();
|
||||
const devices = useAtomValue(deviceCartAtom);
|
||||
const setDeviceCart = useSetAtom(deviceCartAtom);
|
||||
const [months, setMonths] = useAtom(numberOfMonths);
|
||||
const [message, setMessage] = useState("");
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
useEffect(() => {
|
||||
if (months === 7) {
|
||||
setMessage("You will get 1 month free.");
|
||||
} else if (months === 12) {
|
||||
setMessage("You will get 2 months free.");
|
||||
} else {
|
||||
setMessage("");
|
||||
}
|
||||
setTotal(baseAmount + ((devices.length + 1) - 1) * discountPercentage);
|
||||
}, [months, devices.length, baseAmount, discountPercentage]);
|
||||
|
||||
if (pathname === "/payment") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data: PaymentType = {
|
||||
numberOfMonths: months,
|
||||
userId: session?.data?.user.id ?? "",
|
||||
deviceIds: devices.map((device) => device.id),
|
||||
amount: Number.parseFloat(total.toFixed(2)),
|
||||
paid: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-lg mx-auto space-y-4 px-4">
|
||||
<div className="flex max-h-[calc(100svh-400px)] flex-col overflow-auto pb-4 gap-4">
|
||||
{devices.map((device) => (
|
||||
<DeviceCard key={device.id} device={device} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<NumberInput
|
||||
label="Set No of Months"
|
||||
value={months}
|
||||
onChange={(value: number) => setMonths(value)}
|
||||
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}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setDisabled(true);
|
||||
await createPayment(data);
|
||||
setDeviceCart([]);
|
||||
setMonths(1);
|
||||
setDisabled(false);
|
||||
|
||||
}}
|
||||
className="w-full"
|
||||
disabled={devices.length === 0 || disabled}
|
||||
>
|
||||
{disabled ? (
|
||||
<>
|
||||
<Loader2 className="ml-2 animate-spin" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Go to payment
|
||||
<CircleDollarSign />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,20 +1,40 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import React from 'react'
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckCheck, X } from 'lucide-react';
|
||||
|
||||
export default function InputReadOnly({ label, value, labelClassName, className }: { label: string, value: string, labelClassName?: string, className?: string }) {
|
||||
export default function InputReadOnly({ label, value, labelClassName, className, showCheck = true, checkTrue = false }: {
|
||||
label: string;
|
||||
value: string;
|
||||
labelClassName?: string;
|
||||
className?: string;
|
||||
showCheck?: boolean;
|
||||
checkTrue?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("relative rounded-lg border border-input bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-80 [&:has(input:is(:disabled))_*]:pointer-events-none", className)}>
|
||||
<label htmlFor="input-33" className={cn("block px-3 pt-2 text-xs font-medium", labelClassName)}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id="input-33"
|
||||
className="flex h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
|
||||
placeholder={value}
|
||||
disabled
|
||||
value={value}
|
||||
type="text"
|
||||
/>
|
||||
<div className={cn("relative flex items-center justify-between rounded-lg border border-input bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-80 [&:has(input:is(:disabled))_*]:pointer-events-none", className)}>
|
||||
<div>
|
||||
|
||||
<label htmlFor="input-33" className={cn("block px-3 pt-2 text-xs font-medium", labelClassName)}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id="input-33"
|
||||
className="flex h-10 w-full bg-transparent px-3 pb-2 text-sm text-foreground placeholder:text-muted-foreground/70 focus-visible:outline-none"
|
||||
placeholder={value}
|
||||
disabled
|
||||
value={value}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showCheck && (
|
||||
<div>
|
||||
{checkTrue ? (
|
||||
<CheckCheck className='mx-4 text-green-500' />
|
||||
) : (
|
||||
<X className='mx-4 text-red-500' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)
|
||||
|
@ -54,7 +54,7 @@ export default function UserRejectDialog({ user }: { user: User }) {
|
||||
},
|
||||
error: (error) => {
|
||||
setDisabled(false)
|
||||
return error || "Something went wrong"
|
||||
return error.message || "Something went wrong"
|
||||
},
|
||||
})
|
||||
setDisabled(false)
|
||||
|
Reference in New Issue
Block a user