style: add skeletons to paymentId and deviceId pages ♻️
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 10m58s

This commit is contained in:
2025-09-24 17:46:04 +05:00
parent 5dab74b14b
commit 31a05ae917
8 changed files with 501 additions and 382 deletions

View File

@@ -0,0 +1,33 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function DeviceLoading() {
return (
<div>
<div className="flex items-center justify-between title-bg title-bg ring-2 ring-sarLinkOrange/50 rounded-lg p-4">
<div className="flex flex-col space-y-2 justify-between items-start">
<Skeleton className="h-8 w-44" />
<Skeleton className="h-4 w-40" />
<Skeleton className="h-4 w-48" />
</div>
<div className="flex items-center gap-2 flex-col">
<Skeleton className="h-10 w-32" />
</div>
</div>
<div
id="user-filters"
className=" py-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
>
<Skeleton className="h-10 sm:w-64" />
{/* <Filter
options={sortfilterOptions}
defaultOption="asc"
queryParamKey="sortBy"
/> */}
</div>
{/* <Suspense key={query} fallback={"loading...."}>
<DevicesTable searchParams={searchParams} />
</Suspense> */}
</div>
);
}

View File

@@ -1,68 +1,68 @@
import { redirect } from "next/navigation";
import ClientErrorMessage from "@/components/client-error-message"; import ClientErrorMessage from "@/components/client-error-message";
import Search from "@/components/search"; import Search from "@/components/search";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { getDevice } from "@/queries/devices"; import { getDevice } from "@/queries/devices";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
import { redirect } from "next/navigation";
export default async function DeviceDetails({ export default async function DeviceDetails({
params, params,
}: { }: {
params: Promise<{ deviceId: string }>; params: Promise<{ deviceId: string }>;
}) { }) {
const deviceId = (await params)?.deviceId; const deviceId = (await params)?.deviceId;
const [error, device] = await tryCatch(getDevice({ deviceId: deviceId })); const [error, device] = await tryCatch(getDevice({ deviceId: deviceId }));
if (error) { if (error) {
// Handle specific actions for certain errors, but reuse the error message // Handle specific actions for certain errors, but reuse the error message
if (error.message === "UNAUTHORIZED") { if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin"); redirect("/auth/signin");
} else { } else {
// For all other errors, display the error message directly // For all other errors, display the error message directly
return <ClientErrorMessage message={error.message} />; return <ClientErrorMessage message={error.message} />;
} }
} }
if (!device) return null; if (!device) return null;
return ( return (
<div> <div>
<div className="flex items-center justify-between title-bg title-bg ring-2 ring-sarLinkOrange/50 rounded-lg p-2"> <div className="flex items-center justify-between title-bg title-bg ring-2 ring-sarLinkOrange/50 rounded-lg p-4">
<div className="flex flex-col justify-between items-start"> <div className="flex flex-col justify-between items-start">
<h3 className="text-2xl text-sarLinkOrange font-bold"> <h3 className="text-2xl text-sarLinkOrange font-bold">
{device?.name} {device?.name}
</h3> </h3>
<Badge variant={"secondary"}>{device?.mac}</Badge> <Badge variant={"secondary"}>{device?.mac}</Badge>
<p className="text-muted-foreground text-sm mt-2"> <p className="text-muted-foreground text-sm mt-2">
Device active until{" "} Device active until{" "}
{new Date(device?.expiry_date || "").toLocaleDateString("en-US", { {new Date(device?.expiry_date || "").toLocaleDateString("en-US", {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
})} })}
</p> </p>
</div> </div>
<div className="flex items-center gap-2 flex-col"> <div className="flex items-center gap-2 flex-col">
{device?.expiry_date && new Date() < new Date(device.expiry_date) && ( {device?.expiry_date && new Date() < new Date(device.expiry_date) && (
<p className="text-base font-semibold font-mono w-full text-center px-2 p-1 rounded-md bg-green-500/10 text-green-900 dark:text-green-400"> <p className="text-base font-semibold font-mono w-full text-center px-2 p-1 rounded-md bg-green-500/10 text-green-900 dark:text-green-400">
ACTIVE ACTIVE
</p> </p>
)} )}
</div> </div>
</div> </div>
<div <div
id="user-filters" id="user-filters"
className=" py-4 gap-4 flex sm:flex-row flex-col items-start justify-start" className=" py-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
> >
<Search /> <Search />
{/* <Filter {/* <Filter
options={sortfilterOptions} options={sortfilterOptions}
defaultOption="asc" defaultOption="asc"
queryParamKey="sortBy" queryParamKey="sortBy"
/> */} /> */}
</div> </div>
{/* <Suspense key={query} fallback={"loading...."}> {/* <Suspense key={query} fallback={"loading...."}>
<DevicesTable searchParams={searchParams} /> <DevicesTable searchParams={searchParams} />
</Suspense> */} </Suspense> */}
</div> </div>
); );
} }

View File

@@ -2,21 +2,24 @@ import DevicesTableSkeleton from "@/components/device-table-skeleton";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
export default function LoadingComponent() { export default function LoadingComponent() {
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<Skeleton className="w-48 h-8" /> <Skeleton className="w-48 h-8" />
<Skeleton className="w-20 h-8" /> <Skeleton className="w-36 h-8" />
</div> </div>
<div <div>
id="user-filters" <Skeleton className="w-full rounded-md mt-5 mb-6 sm:w-48 h-9" />
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO" </div>
> <div
<DevicesTableSkeleton id="user-filters"
headers={["Device Name", "Mac Address", "Vendor", "#"]} className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
length={10} >
/> <DevicesTableSkeleton
</div> headers={["Device Name", "Mac Address", "Vendor", "#"]}
</div> length={10}
); />
</div>
</div>
);
} }

View File

@@ -0,0 +1,63 @@
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableRow,
} from "@/components/ui/table";
export default function PaymentLoading() {
return (
<div className="mx-2">
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4">
<Skeleton className="h-8 w-48" />
</div>
<div className="flex flex-col gap-4 mt-7 w-full border rounded-md border-dashed title-bg py-3 px-2 mb-3">
<Skeleton className="h-5 w-36" />
</div>
<div className="flex flex-col gap-3 w-full">
{Array.from({ length: 1 }).map((_, i) => (
<Skeleton
key={`${i + 1}`}
className="w-full border border-gray-300 h-13 rounded-sm"
/>
))}
<div className="pb-4 w-full gap-4 flex sm:flex-row flex-col items-start justify-start">
<div className="my-1 w-full flex items-center justify-between p-2 text-sm text-foreground border rounded">
<Table>
<TableBody className="">
<TableRow>
<TableCell>Payment created</TableCell>
<TableCell className="text-right">
<Skeleton className="h-5 inline-block w-24" />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Total Devices</TableCell>
<TableCell className="text-right text-xl">
<Skeleton className="h-5 w-24 inline-block" />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Duration</TableCell>
<TableCell className="text-right text-xl">
<Skeleton className="h-5 w-24 inline-block" />
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow className="">
<TableCell colSpan={1}>Total Due</TableCell>
<TableCell className="text-right text-3xl font-bold">
<Skeleton className="h-5 w-24 inline-block" />
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</div>
</div>
</div>
);
}

View File

@@ -10,77 +10,77 @@ import { TextShimmer } from "@/components/ui/text-shimmer";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
export default async function PaymentPage({ export default async function PaymentPage({
params, params,
}: { }: {
params: Promise<{ paymentId: string }>; params: Promise<{ paymentId: string }>;
}) { }) {
const paymentId = (await params).paymentId; const paymentId = (await params).paymentId;
const [error, payment] = await tryCatch(getPayment({ id: paymentId })); const [error, payment] = await tryCatch(getPayment({ id: paymentId }));
if (error) { if (error) {
if (error.message === "Invalid token.") redirect("/auth/signin"); if (error.message === "Invalid token.") redirect("/auth/signin");
return <ClientErrorMessage message={error.message} />; return <ClientErrorMessage message={error.message} />;
} }
const [userError, userProfile] = await tryCatch(getProfile()); const [userError, userProfile] = await tryCatch(getProfile());
if (userError) { if (userError) {
if (userError.message === "Invalid token.") redirect("/auth/signin"); if (userError.message === "Invalid token.") redirect("/auth/signin");
return <ClientErrorMessage message={userError.message} />; return <ClientErrorMessage message={userError.message} />;
} }
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-4 mb-4 mx-2">
<h3 className="text-sarLinkOrange text-2xl">Payment</h3> <h3 className="text-sarLinkOrange text-2xl">Payment</h3>
<div className="flex flex-col gap-4 items-end w-full"> <div className="flex flex-col gap-4 items-end w-full">
{!payment.is_expired && {!payment.is_expired &&
payment.paid && payment.paid &&
payment.status !== "PENDING" && ( payment.status !== "PENDING" && (
<Button <Button
disabled disabled
className={cn( className={cn(
"rounded-md opacity-100! uppercase font-semibold", "rounded-md opacity-100! uppercase font-semibold",
payment?.paid payment?.paid
? "text-green-900 bg-green-500/20" ? "text-green-900 bg-green-500/20"
: "text-inherit bg-yellow-400", : "text-inherit bg-yellow-400",
)} )}
> >
{payment.status} {payment.status}
</Button> </Button>
)} )}
{payment.status === "PENDING" && !payment.is_expired && ( {payment.status === "PENDING" && !payment.is_expired && (
<Button> <Button>
<TextShimmer>Payment Pending</TextShimmer>{" "} <TextShimmer>Payment Pending</TextShimmer>{" "}
</Button> </Button>
)} )}
{!payment.paid && {!payment.paid &&
(payment.is_expired ? ( (payment.is_expired ? (
<Button <Button
disabled disabled
className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20" className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20"
> >
Payment Expired Payment Expired
</Button> </Button>
) : payment.status === "PENDING" ? ( ) : payment.status === "PENDING" ? (
<CancelPaymentButton paymentId={paymentId} /> <CancelPaymentButton paymentId={paymentId} />
) : payment.status === "CANCELLED" ? ( ) : payment.status === "CANCELLED" ? (
<Button disabled>Payment Cancelled</Button> <Button disabled>Payment Cancelled</Button>
) : ( ) : (
"" ""
))} ))}
</div> </div>
</div> </div>
{!payment.paid && ( {!payment.paid && (
<ExpiryCountDown expiryLabel="Payment" expiresAt={payment.expires_at} /> <ExpiryCountDown expiryLabel="Payment" expiresAt={payment.expires_at} />
)} )}
<div <div
id="user-device-payments" id="user-device-payments"
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start" className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
> >
<DevicesToPay <DevicesToPay
disabled={payment.paid || payment.is_expired} disabled={payment.paid || payment.is_expired}
user={userProfile || undefined} user={userProfile || undefined}
payment={payment || undefined} payment={payment || undefined}
/> />
</div> </div>
</div> </div>
); );
} }

View File

@@ -2,21 +2,24 @@ import DevicesTableSkeleton from "@/components/device-table-skeleton";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
export default function LoadingComponent() { export default function LoadingComponent() {
return ( return (
<div> <div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> <div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<Skeleton className="w-48 h-8" /> <Skeleton className="w-48 h-8" />
<Skeleton className="w-20 h-8" /> <Skeleton className="w-20 h-8" />
</div> </div>
<div <div>
id="user-filters" <Skeleton className="w-full rounded-md mt-5 mb-6 sm:w-48 h-9" />
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO" </div>
> <div
<DevicesTableSkeleton id="user-filters"
headers={["Details", "Duration", "Status", "Amount"]} className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
length={10} >
/> <DevicesTableSkeleton
</div> headers={["Details", "Duration", "Status", "Amount"]}
</div> length={10}
); />
</div>
</div>
);
} }

View File

@@ -78,11 +78,13 @@ export default function DevicesToPay({
{devices?.map((device) => ( {devices?.map((device) => (
<div <div
key={device.id} key={device.id}
className="bg-muted border rounded p-2 flex gap-2 items-center" className="bg-muted border rounded p-2 flex gap-2 items-center motion-preset-fade"
> >
<div className="flex flex-col"> <div className="flex flex-col">
<div className="text-sm font-medium">{device.name}</div> <div className="text-sm font-medium motion-preset-slide-up">
<div className="text-xs text-muted-foreground"> {device.name}
</div>
<div className="text-xs text-muted-foreground motion-preset-slide-up motion-delay-100">
{device.mac} {device.mac}
</div> </div>
</div> </div>
@@ -160,8 +162,10 @@ export default function DevicesToPay({
</TableCaption> </TableCaption>
<TableBody className=""> <TableBody className="">
<TableRow> <TableRow>
<TableCell>Payment created</TableCell> <TableCell className="motion-preset-slide-left-sm">
<TableCell className="text-right"> Payment created
</TableCell>
<TableCell className="text-right motion-preset-slide-right-sm">
{new Date(payment?.created_at ?? "").toLocaleDateString( {new Date(payment?.created_at ?? "").toLocaleDateString(
"en-US", "en-US",
{ {
@@ -176,22 +180,31 @@ export default function DevicesToPay({
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell>Total Devices</TableCell> <TableCell className="motion-preset-slide-left-sm motion-delay-75">
<TableCell className="text-right text-xl"> Total Devices
</TableCell>
<TableCell className="text-right text-xl motion-preset-slide-right-sm motion-delay-75">
{devices?.length} {devices?.length}
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell>Duration</TableCell> <TableCell className="motion-preset-slide-left-sm motion-delay-100">
<TableCell className="text-right text-xl"> Duration
</TableCell>
<TableCell className="text-right text-xl motion-preset-slide-right-sm motion-delay-100">
{payment?.number_of_months} Months {payment?.number_of_months} Months
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow className=""> <TableRow className="">
<TableCell colSpan={1}>Total Due</TableCell> <TableCell
<TableCell className="text-right text-3xl font-bold"> className="motion-preset-slide-left-sm motion-delay-150"
colSpan={1}
>
Total Due
</TableCell>
<TableCell className="text-right text-3xl font-bold motion-preset-slide-right-sm motion-delay-150">
{payment?.amount?.toFixed(2)} {payment?.amount?.toFixed(2)}
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -2,13 +2,13 @@ import { Calendar } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableFooter, TableFooter,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import type { WalletTransaction } from "@/lib/backend-types"; import type { WalletTransaction } from "@/lib/backend-types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -19,223 +19,227 @@ import { Badge } from "./ui/badge";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
export async function WalletTransactionsTable({ export async function WalletTransactionsTable({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ searchParams: Promise<{
[key: string]: unknown; [key: string]: unknown;
}>; }>;
}) { }) {
const resolvedParams = await searchParams; const resolvedParams = await searchParams;
const page = Number.parseInt(resolvedParams.page as string) || 1; const page = Number.parseInt(resolvedParams.page as string) || 1;
const limit = 10; const limit = 10;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
// Build params object // Build params object
const apiParams: Record<string, string | number | undefined> = {}; const apiParams: Record<string, string | number | undefined> = {};
for (const [key, value] of Object.entries(resolvedParams)) { for (const [key, value] of Object.entries(resolvedParams)) {
if (value !== undefined && value !== "") { if (value !== undefined && value !== "") {
apiParams[key] = typeof value === "number" ? value : String(value); apiParams[key] = typeof value === "number" ? value : String(value);
} }
} }
apiParams.limit = limit; apiParams.limit = limit;
apiParams.offset = offset; apiParams.offset = offset;
const [error, transactions] = await tryCatch( const [error, transactions] = await tryCatch(
getWaleltTransactions(apiParams), getWaleltTransactions(apiParams),
); );
if (error) { if (error) {
if (error.message.includes("Unauthorized")) { if (error.message.includes("Unauthorized")) {
redirect("/auth/signin"); redirect("/auth/signin");
} else { } else {
return <pre>{JSON.stringify(error, null, 2)}</pre>; return <pre>{JSON.stringify(error, null, 2)}</pre>;
} }
} }
const { data, meta } = transactions; const { data, meta } = transactions;
const totalDebit = data.reduce( const totalDebit = data.reduce(
(acc, trx) => acc + (trx.transaction_type === "DEBIT" ? trx.amount : 0), (acc, trx) => acc + (trx.transaction_type === "DEBIT" ? trx.amount : 0),
0, 0,
); );
const totalCredit = data.reduce( const totalCredit = data.reduce(
(acc, trx) => acc + (trx.transaction_type === "TOPUP" ? trx.amount : 0), (acc, trx) => acc + (trx.transaction_type === "TOPUP" ? trx.amount : 0),
0, 0,
); );
return ( return (
<div> <div>
{data?.length === 0 ? ( {data?.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4"> <div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No transactions yet.</h3> <h3>No transactions yet.</h3>
</div> </div>
) : ( ) : (
<div> <div>
<div className="flex gap-4 mb-4 w-full"> <div className="flex gap-4 mb-4 w-full">
<div className="bg-red-400 w-full sm:w-fit dark:bg-red-950 dark:text-red-400 text-red-900 p-2 px-4 rounded-md mb-2"> <div className="bg-red-300 ring-4 ring-red-500/20 w-full sm:w-fit dark:bg-red-950 dark:text-red-400 text-red-900 p-2 px-4 rounded-md mb-2">
<h5 className="text-lg font-semibold">Total Debit</h5> <h5 className="text-lg font-semibold uppercase font-barlow">
<p>{totalDebit.toFixed(2)} MVR</p> Total Debit
</div> </h5>
<div className="bg-green-400 w-full sm:w-fit dark:bg-green-950 dark:text-green-400 text-green-900 p-2 px-4 rounded-md mb-2"> <p>{totalDebit.toFixed(2)} MVR</p>
<h5 className="text-lg font-semibold">Total Credit</h5> </div>
<p>{totalCredit.toFixed(2)} MVR</p> <div className="bg-green-300 ring-4 ring-green-500/20 w-full sm:w-fit dark:bg-green-950 dark:text-green-400 text-green-900 p-2 px-4 rounded-md mb-2">
</div> <h5 className="text-lg font-semibold uppercase font-barlow">
</div> Total Credit
<div className="hidden sm:block"> </h5>
<Table className="overflow-scroll"> <p>{totalCredit.toFixed(2)} MVR</p>
<TableHeader> </div>
<TableRow> </div>
<TableHead>Description</TableHead> <div className="hidden sm:block">
<TableHead>Amount</TableHead> <Table className="overflow-scroll">
<TableHead>Transaction Type</TableHead> <TableHeader>
<TableHead>View Details</TableHead> <TableRow>
<TableHead>Created at</TableHead> <TableHead>Description</TableHead>
</TableRow> <TableHead>Amount</TableHead>
</TableHeader> <TableHead>Transaction Type</TableHead>
<TableBody className="overflow-scroll"> <TableHead>View Details</TableHead>
{transactions?.data?.map((trx) => ( <TableHead>Created at</TableHead>
<TableRow </TableRow>
className={cn( </TableHeader>
"items-start border rounded p-2", <TableBody className="overflow-scroll">
trx?.transaction_type === "TOPUP" {transactions?.data?.map((trx) => (
? "credit-bg" <TableRow
: "debit-bg", className={cn(
)} "items-start border rounded p-2",
key={trx.id} trx?.transaction_type === "TOPUP"
> ? "credit-bg"
<TableCell> : "debit-bg",
<span className="text-muted-foreground"> )}
{trx.description} key={trx.id}
</span> >
</TableCell> <TableCell>
<TableCell>{trx.amount.toFixed(2)} MVR</TableCell> <span className="text-muted-foreground">
{trx.description}
</span>
</TableCell>
<TableCell>{trx.amount.toFixed(2)} MVR</TableCell>
<TableCell> <TableCell>
<span className="font-semibold pr-2"> <span className="font-semibold pr-2">
{trx.transaction_type === "TOPUP" ? ( {trx.transaction_type === "TOPUP" ? (
<Badge className="bg-green-100 text-green-950 dark:bg-green-700"> <Badge className="bg-green-100 text-green-950 dark:bg-green-700">
{trx.transaction_type} {trx.transaction_type}
</Badge> </Badge>
) : ( ) : (
<Badge className="bg-red-500 text-red-950 dark:bg-red-700"> <Badge className="bg-red-500 text-red-950 dark:bg-red-700">
{trx.transaction_type} {trx.transaction_type}
</Badge> </Badge>
)} )}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className=""> <span className="">
{new Date(trx.created_at).toLocaleDateString("en-US", { {new Date(trx.created_at).toLocaleDateString("en-US", {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
minute: "2-digit", minute: "2-digit",
hour: "2-digit", hour: "2-digit",
})} })}
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button> <Button>
<Link <Link
className="font-medium " className="font-medium "
href={ href={
trx.transaction_type === "TOPUP" trx.transaction_type === "TOPUP"
? `/top-ups/${trx.reference_id}` ? `/top-ups/${trx.reference_id}`
: `/payments/${trx.reference_id}` : `/payments/${trx.reference_id}`
} }
> >
View Details View Details
</Link> </Link>
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-muted-foreground"> <TableCell colSpan={5} className="text-muted-foreground">
{meta?.total === 1 ? ( {meta?.total === 1 ? (
<p className="text-center"> <p className="text-center">
Total {meta?.total} transaction. Total {meta?.total} transaction.
</p> </p>
) : ( ) : (
<p className="text-center"> <p className="text-center">
Total {meta?.total} transactions. Total {meta?.total} transactions.
</p> </p>
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
</div> </div>
<div className="sm:hidden block"> <div className="sm:hidden block">
{data.map((trx) => ( {data.map((trx) => (
<MobileTransactionDetails key={trx.id} trx={trx} /> <MobileTransactionDetails key={trx.id} trx={trx} />
))} ))}
</div> </div>
<Pagination <Pagination
totalPages={meta?.last_page} totalPages={meta?.last_page}
currentPage={meta?.current_page} currentPage={meta?.current_page}
/> />
</div> </div>
)} )}
</div> </div>
); );
} }
function MobileTransactionDetails({ trx }: { trx: WalletTransaction }) { function MobileTransactionDetails({ trx }: { trx: WalletTransaction }) {
return ( return (
<div <div
className={cn( className={cn(
"flex flex-col items-start border rounded p-2 my-2", "flex flex-col items-start border rounded p-2 my-2",
trx?.transaction_type === "TOPUP" ? "credit-bg" : "debit-bg", trx?.transaction_type === "TOPUP" ? "credit-bg" : "debit-bg",
)} )}
> >
<div className="bg-white shadow dark:bg-black p-2 rounded w-full"> <div className="bg-white shadow dark:bg-black p-2 rounded w-full">
<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 text-sm"> <span className="text-muted-foreground text-sm">
{new Date(trx.created_at).toLocaleDateString("en-US", { {new Date(trx.created_at).toLocaleDateString("en-US", {
month: "short", month: "short",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
minute: "2-digit", minute: "2-digit",
hour: "2-digit", hour: "2-digit",
})} })}
</span> </span>
</div> </div>
<p className="text-sm text-muted-foreground py-4">{trx.description}</p> <p className="text-sm text-muted-foreground py-4">{trx.description}</p>
</div> </div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border flex justify-between items-center"> <div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border flex justify-between items-center">
<div className="block sm:hidden"> <div className="block sm:hidden">
<h3 className="text-sm font-medium">Amount</h3> <h3 className="text-sm font-medium">Amount</h3>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{trx.amount.toFixed(2)} MVR {trx.amount.toFixed(2)} MVR
</span> </span>
</div> </div>
<span className="font-semibold pr-2"> <span className="font-semibold pr-2">
{trx.transaction_type === "TOPUP" ? ( {trx.transaction_type === "TOPUP" ? (
<Badge className="bg-green-100 text-green-950 dark:bg-green-700"> <Badge className="bg-green-100 text-green-950 dark:bg-green-700">
{trx.transaction_type} {trx.transaction_type}
</Badge> </Badge>
) : ( ) : (
<Badge className="bg-red-500 text-red-950 dark:bg-red-700"> <Badge className="bg-red-500 text-red-950 dark:bg-red-700">
{trx.transaction_type} {trx.transaction_type}
</Badge> </Badge>
)} )}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 mt-2 w-full"> <div className="flex items-center gap-2 mt-2 w-full">
<Link <Link
className="font-medium hover:underline" className="font-medium hover:underline"
href={ href={
trx.transaction_type === "TOPUP" trx.transaction_type === "TOPUP"
? `/top-ups/${trx.reference_id}` ? `/top-ups/${trx.reference_id}`
: `/payments/${trx.reference_id}` : `/payments/${trx.reference_id}`
} }
> >
<Button size={"sm"} className="w-full"> <Button size={"sm"} className="w-full">
View Details View Details
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
); );
} }