chore: add skeletons to tables and loading.tsx files for routes and run formatting ♻️
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 12m20s

This commit is contained in:
2025-09-20 20:42:14 +05:00
parent 5277c13fb7
commit a60e9a9c85
45 changed files with 3539 additions and 3041 deletions

View File

@@ -3,27 +3,27 @@ import { AgreementCard } from "@/components/agreement-card";
import { tryCatch } from "@/utils/tryCatch";
export default async function Agreements() {
const [error, profile] = await tryCatch(getProfile());
return (
<div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">Agreements</h3>
</div>
<div className="grid grid-cols-1">
{error ? (
<div className="text-red-500">
An error occurred while fetching agreements: {error.message}
</div>
) : (
<div>
{profile.agreement ? (
<AgreementCard agreement={profile.agreement} />
) : (
<div className="text-gray-500">No agreement found.</div>
)}
</div>
)}
</div>
</div>
);
const [error, profile] = await tryCatch(getProfile());
return (
<div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">Agreements</h3>
</div>
<div className="grid grid-cols-1">
{error ? (
<div className="text-red-500">
An error occurred while fetching agreements: {error.message}
</div>
) : (
<div>
{profile.agreement ? (
<AgreementCard agreement={profile.agreement} />
) : (
<div className="text-gray-500">No agreement found.</div>
)}
</div>
)}
</div>
</div>
);
}

View File

@@ -1,76 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
export default function DevicesTableSkeleton() {
return (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableHeader>
<TableRow>
<TableHead>Device Name</TableHead>
<TableHead>MAC Address</TableHead>
<TableHead>#</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{Array.from({ length: 10 }).map((_, i) => (
<TableRow key={`${i + 1}`}>
<TableCell>
<Skeleton className="w-full h-10 rounded" />
</TableCell>
<TableCell>
<Skeleton className="w-full h-10 rounded" />
</TableCell>
<TableCell>
<Skeleton className="w-full h-10 rounded" />
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>
<Skeleton className="w-full h-4 rounded" />
</TableCell>
<TableCell className="text-muted-foreground">
<Skeleton className="w-20 h-4 rounded" />
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
<div className="sm:hidden my-4">
{Array.from({ length: 10 }).map((_, i) => (
<DeviceCardSkeleton key={`${i + 1}`} />
))}
</div>
</>
);
}
function DeviceCardSkeleton() {
return (
<div
className={cn(
"flex text-sm justify-between items-center my-2 p-4 border rounded-md bg-gray-100",
)}
>
<div className="font-semibold flex w-full flex-col items-start gap-2 mb-2 relative">
<Skeleton className="w-32 h-6" />
<Skeleton className="w-36 h-6" />
<Skeleton className="w-32 h-4" />
<Skeleton className="w-40 h-8" />
</div>
</div>
);
}

View File

@@ -0,0 +1,22 @@
import DevicesTableSkeleton from "@/components/device-table-skeleton";
import { Skeleton } from "@/components/ui/skeleton";
export default function LoadingComponent() {
return (
<div>
<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-20 h-8" />
</div>
<div
id="user-filters"
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
>
<DevicesTableSkeleton
headers={["Device Name", "Mac Address", "Vendor", "#"]}
length={10}
/>
</div>
</div>
);
}

View File

@@ -1,10 +1,10 @@
import { getServerSession } from "next-auth";
import { Suspense } from "react";
import { authOptions } from "@/app/auth";
import DevicesTableSkeleton from "@/components/device-table-skeleton";
import { DevicesTable } from "@/components/devices-table";
import DynamicFilter from "@/components/generic-filter";
import AddDeviceDialogForm from "@/components/user/add-device-dialog";
import DevicesTableSkeleton from "./device-table-skeleton";
export default async function Devices({
searchParams,
@@ -53,7 +53,15 @@ export default async function Devices({
]}
/>
</div>
<Suspense key={query || page} fallback={<DevicesTableSkeleton />}>
<Suspense
key={query || page}
fallback={
<DevicesTableSkeleton
headers={["Device Name", "Mac Address", "Vendor", "#"]}
length={10}
/>
}
>
<DevicesTable parentalControl={false} searchParams={searchParams} />
</Suspense>
</div>

View File

@@ -0,0 +1,22 @@
import DevicesTableSkeleton from "@/components/device-table-skeleton";
import { Skeleton } from "@/components/ui/skeleton";
export default function LoadingComponent() {
return (
<div>
<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-20 h-8" />
</div>
<div
id="user-filters"
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
>
<DevicesTableSkeleton
headers={["Device Name", "Mac Address", "Vendor", "#"]}
length={10}
/>
</div>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import { Suspense } from "react";
import DevicesTableSkeleton from "@/components/device-table-skeleton";
import { DevicesTable } from "@/components/devices-table";
import DynamicFilter from "@/components/generic-filter";
@@ -51,7 +52,15 @@ export default async function ParentalControl({
]}
/>{" "}
</div>
<Suspense key={(await searchParams).page} fallback={"loading...."}>
<Suspense
key={(await searchParams).page}
fallback={
<DevicesTableSkeleton
headers={["Device Name", "Mac Address", "Vendor", "#"]}
length={10}
/>
}
>
<DevicesTable
parentalControl={true}
searchParams={searchParams}

View File

@@ -0,0 +1,22 @@
import DevicesTableSkeleton from "@/components/device-table-skeleton";
import { Skeleton } from "@/components/ui/skeleton";
export default function LoadingComponent() {
return (
<div>
<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-20 h-8" />
</div>
<div
id="user-filters"
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
>
<DevicesTableSkeleton
headers={["Details", "Duration", "Status", "Amount"]}
length={10}
/>
</div>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import { Suspense } from "react";
import DevicesTableSkeleton from "@/components/device-table-skeleton";
import DynamicFilter from "@/components/generic-filter";
import { PaymentsTable } from "@/components/payments-table";
@@ -14,8 +15,8 @@ export default async function Payments({
return (
<div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">My Payments</h3>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-3 mb-4">
<h3 className="text-sarLinkOrange text-2xl">My Subscriptions</h3>
</div>
<div
id="user-filters"
@@ -72,7 +73,15 @@ export default async function Payments({
]}
/>{" "}
</div>
<Suspense key={query} fallback={"loading...."}>
<Suspense
key={query}
fallback={
<DevicesTableSkeleton
headers={["Details", "Duration", "Status", "Amount"]}
length={10}
/>
}
>
<PaymentsTable searchParams={searchParams} />
</Suspense>
</div>

View File

@@ -9,72 +9,72 @@ import { TextShimmer } from "@/components/ui/text-shimmer";
import { cn } from "@/lib/utils";
import { tryCatch } from "@/utils/tryCatch";
export default async function TopupPage({
params,
params,
}: {
params: Promise<{ topupId: string }>;
params: Promise<{ topupId: string }>;
}) {
const topupId = (await params).topupId;
const [error, topup] = await tryCatch(getTopup({ id: topupId }));
if (error) {
if (error.message === "Invalid token.") redirect("/auth/signin");
return <ClientErrorMessage message={error.message} />;
}
const topupId = (await params).topupId;
const [error, topup] = await tryCatch(getTopup({ id: topupId }));
if (error) {
if (error.message === "Invalid token.") redirect("/auth/signin");
return <ClientErrorMessage message={error.message} />;
}
return (
<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">
<h3 className="text-sarLinkOrange text-2xl">Topup</h3>
<div className="flex flex-col gap-4 items-end w-full">
{!topup.is_expired && topup.paid && topup.status !== "PENDING" && (
<Button
disabled
className={cn(
"rounded-md opacity-100! uppercase font-semibold",
topup?.paid
? "text-green-900 bg-green-500/20"
: "text-inherit bg-yellow-400",
)}
>
{topup.status}
</Button>
)}
{topup.status === "PENDING" && !topup.is_expired && (
<Button>
<TextShimmer>Payment Pending</TextShimmer>{" "}
</Button>
)}
return (
<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">
<h3 className="text-sarLinkOrange text-2xl">Topup</h3>
<div className="flex flex-col gap-4 items-end w-full">
{!topup.is_expired && topup.paid && topup.status !== "PENDING" && (
<Button
disabled
className={cn(
"rounded-md opacity-100! uppercase font-semibold",
topup?.paid
? "text-green-900 bg-green-500/20"
: "text-inherit bg-yellow-400",
)}
>
{topup.status}
</Button>
)}
{topup.status === "PENDING" && !topup.is_expired && (
<Button>
<TextShimmer>Payment Pending</TextShimmer>{" "}
</Button>
)}
{!topup.paid &&
(topup.is_expired ? (
<Button
disabled
className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20"
>
Topup Expired
</Button>
) : topup.status === "PENDING" ? (
<CancelTopupButton topupId={topupId} />
) : topup.status === "CANCELLED" ? (
<Button disabled>Topup Cancelled</Button>
) : (
""
))}
</div>
</div>
{!topup.paid && topup.status === "PENDING" && !topup.is_expired && (
<ExpiryCountDown expiryLabel="Top up" expiresAt={topup.expires_at} />
)}
<div
id="user-topup-details"
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
>
<TopupToPay
disabled={
topup.paid || topup.is_expired || topup.status === "CANCELLED"
}
topup={topup || undefined}
/>
</div>
</div>
);
{!topup.paid &&
(topup.is_expired ? (
<Button
disabled
className="rounded-md opacity-100! uppercase font-semibold text-red-500 bg-red-500/20"
>
Topup Expired
</Button>
) : topup.status === "PENDING" ? (
<CancelTopupButton topupId={topupId} />
) : topup.status === "CANCELLED" ? (
<Button disabled>Topup Cancelled</Button>
) : (
""
))}
</div>
</div>
{!topup.paid && topup.status === "PENDING" && !topup.is_expired && (
<ExpiryCountDown expiryLabel="Top up" expiresAt={topup.expires_at} />
)}
<div
id="user-topup-details"
className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
>
<TopupToPay
disabled={
topup.paid || topup.is_expired || topup.status === "CANCELLED"
}
topup={topup || undefined}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,22 @@
import DevicesTableSkeleton from "@/components/device-table-skeleton";
import { Skeleton } from "@/components/ui/skeleton";
export default function LoadingComponent() {
return (
<div>
<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-20 h-8" />
</div>
<div
id="user-filters"
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-endO"
>
<DevicesTableSkeleton
headers={["Details", "Status", "Amount"]}
length={10}
/>
</div>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import { Suspense } from "react";
import DevicesTableSkeleton from "@/components/device-table-skeleton";
import DynamicFilter from "@/components/generic-filter";
import { TopupsTable } from "@/components/topups-table";
@@ -78,7 +79,15 @@ export default async function Topups({
]}
/>
</div>
<Suspense key={query} fallback={"loading...."}>
<Suspense
key={query}
fallback={
<DevicesTableSkeleton
headers={["Details", "Status", "Amount"]}
length={10}
/>
}
>
<TopupsTable searchParams={searchParams} />
</Suspense>
</div>

View File

@@ -1,63 +1,78 @@
import { Suspense } from "react";
import DevicesTableSkeleton from "@/components/device-table-skeleton";
import DynamicFilter from "@/components/generic-filter";
import { WalletTransactionsTable } from "@/components/wallet-transactions-table";
export default async function Wallet({
searchParams,
searchParams,
}: {
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
status: string;
}>;
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
status: string;
}>;
}) {
const query = (await searchParams)?.query || "";
const query = (await searchParams)?.query || "";
return (
<div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">Transaction History</h3>
</div>
<div
id="wallet-filters"
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
>
<DynamicFilter
inputs={[
{
label: "Type",
name: "transaction_type",
type: "radio-group",
options: [
{
label: "All",
value: "",
},
{
label: "Debit",
value: "debit",
},
{
label: "Credit",
value: "credit",
},
],
},
{
label: "Topup Amount",
name: "amount",
type: "dual-range-slider",
min: 0,
max: 1000,
step: 10,
},
]}
/>
</div>
<Suspense key={query} fallback={"loading...."}>
<WalletTransactionsTable searchParams={searchParams} />
</Suspense>
</div>
);
return (
<div>
<div className="flex justify-between items-center border rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4">
<h3 className="text-sarLinkOrange text-2xl">Transaction History</h3>
</div>
<div
id="wallet-filters"
className=" pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start"
>
<DynamicFilter
inputs={[
{
label: "Type",
name: "transaction_type",
type: "radio-group",
options: [
{
label: "All",
value: "",
},
{
label: "Debit",
value: "debit",
},
{
label: "Credit",
value: "credit",
},
],
},
{
label: "Topup Amount",
name: "amount",
type: "dual-range-slider",
min: 0,
max: 1000,
step: 10,
},
]}
/>
</div>
<Suspense
key={query}
fallback={
<DevicesTableSkeleton
headers={[
"Description",
"Amount",
"Transaction Type",
"View Details",
"Created at",
]}
length={10}
/>
}
>
<WalletTransactionsTable searchParams={searchParams} />
</Suspense>
</div>
);
}