feat: add loading state and full-page loader component; update payment page and application layout to improve user experience
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m23s

This commit is contained in:
i701 2025-05-31 12:37:46 +05:00
parent c705addccc
commit bed426a6b4
Signed by: i701
GPG Key ID: 54A0DA1E26D8E587
14 changed files with 84 additions and 18 deletions

View File

@ -0,0 +1,8 @@
import FullPageLoader from '@/components/full-page-loader'
import React from 'react'
export default function Loading() {
return (
<FullPageLoader />
)
}

View File

@ -39,7 +39,9 @@ export default async function PaymentPage({
> >
{payment?.paid ? "Paid" : "Pending"} {payment?.paid ? "Paid" : "Pending"}
</Button> </Button>
{!payment.paid && (
<CancelPaymentButton paymentId={paymentId} /> <CancelPaymentButton paymentId={paymentId} />
)}
</div> </div>
</div> </div>

View File

@ -16,6 +16,7 @@ import { tryCatch } from "@/utils/tryCatch";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { AccountPopover } from "./account-popver"; import { AccountPopover } from "./account-popver";
import { WelcomeBanner } from "../welcome-banner";
export async function ApplicationLayout({ export async function ApplicationLayout({
children, children,
@ -31,7 +32,6 @@ export async function ApplicationLayout({
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />
<DeviceCartDrawer />
<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 ">
@ -45,13 +45,14 @@ export async function ApplicationLayout({
<AccountPopover /> <AccountPopover />
</div> </div>
</header> </header>
<div className="text-sm font-mono px-2 p-1 bg-green-500/10 text-green-900 dark:text-green-400"> <WelcomeBanner
Welcome,{" "} firstName={session?.user?.first_name}
<span className="font-semibold"> lastName={session?.user?.last_name}
{session?.user?.first_name} {session?.user?.last_name} />
</span> <DeviceCartDrawer />
<div className="p-4 flex flex-col flex-1 rounded-lg bg-background">
{children}
</div> </div>
<div className="p-4 flex flex-col flex-1">{children}</div>
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>
); );

View File

@ -20,7 +20,7 @@ export default function ClickableRow({
key={device.id} key={device.id}
className={cn( className={cn(
(parentalControl === false && device.blocked) || device.is_active (parentalControl === false && device.blocked) || device.is_active
? "cursor-not-allowed bg-accent-foreground/10 hover:bg-accent-foreground/10" ? "cursor-not-allowed hover:bg-accent-foreground/10"
: "cursor-pointer hover:bg-muted-foreground/10", : "cursor-pointer hover:bg-muted-foreground/10",
)} )}
onClick={() => { onClick={() => {

View File

@ -19,7 +19,7 @@ export default function DeviceCard({
return ( return (
<div <div
onKeyUp={() => {}} onKeyUp={() => { }}
onClick={() => { onClick={() => {
if (device.blocked) return; if (device.blocked) return;
if (device.is_active === true) return; if (device.is_active === true) return;
@ -36,9 +36,9 @@ export default function DeviceCard({
<div <div
className={cn( className={cn(
"flex text-sm justify-between items-center my-2 p-4 border rounded-md", "flex text-sm justify-between items-center my-2 p-4 border rounded-md",
isChecked ? "bg-accent" : "bg-", isChecked ? "bg-accent" : "",
device.is_active device.is_active
? "cursor-not-allowed bg-accent-foreground/10 hover:bg-accent-foreground/10" ? "cursor-not-allowed text-green-600 hover:bg-accent-foreground/10"
: "cursor-pointer hover:bg-muted-foreground/10", : "cursor-pointer hover:bg-muted-foreground/10",
)} )}
> >

View File

@ -10,7 +10,6 @@ export function DeviceCartDrawer() {
const pathname = usePathname(); const pathname = usePathname();
const devices = useAtomValue(deviceCartAtom); const devices = useAtomValue(deviceCartAtom);
const router = useRouter(); const router = useRouter();
if (pathname === "/payment" || pathname === "/devices-to-pay") { if (pathname === "/payment" || pathname === "/devices-to-pay") {
return null; return null;
} }
@ -19,7 +18,7 @@ export function DeviceCartDrawer() {
return ( return (
<Button <Button
size={"lg"} size={"lg"}
className="bg-sarLinkOrange fixed bottom-20 w-80 uppercase h-12 z-20 left-1/2 transform -translate-x-1/2" className="bg-sarLinkOrange dark:hover:bg-orange-900 fixed bottom-20 w-80 uppercase h-12 z-20 left-1/2 transform -translate-x-1/2 hover:ring-2 hover:ring-sarLinkOrange transition-all duration-200"
onClick={() => router.push("/devices-to-pay")} onClick={() => router.push("/devices-to-pay")}
variant="outline" variant="outline"
> >

View File

@ -12,6 +12,7 @@ import { CircleDollarSign, Loader2 } from "lucide-react";
import { redirect, usePathname } from "next/navigation"; import { redirect, usePathname } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import FullPageLoader from "./full-page-loader";
export default function DevicesForPayment() { export default function DevicesForPayment() {
const baseAmount = 100; const baseAmount = 100;
const discountPercentage = 75; const discountPercentage = 75;
@ -45,7 +46,7 @@ export default function DevicesForPayment() {
}; };
if (disabled) { if (disabled) {
return "Please wait..."; return <FullPageLoader />
} }
return ( return (
<div className="max-w-lg mx-auto space-y-4 px-4"> <div className="max-w-lg mx-auto space-y-4 px-4">

View File

@ -0,0 +1,9 @@
import React from 'react'
import { Loader2 } from 'lucide-react'
export default function FullPageLoader() {
return (
<div className='flex items-center justify-center h-screen'>
<Loader2 className='animate-spin' />
</div>
)
}

View File

@ -101,7 +101,7 @@ export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) {
Device Name Device Name
</Label> </Label>
<Input <Input
placeholder="eg: Iphone X" placeholder="eg: iPhone X"
type="text" type="text"
{...register("name")} {...register("name")}
id="device_name" id="device_name"

View File

@ -0,0 +1,33 @@
"use client";
import { useEffect, useState } from "react";
interface WelcomeBannerProps {
firstName?: string | null;
lastName?: string | null;
}
export function WelcomeBanner({ firstName, lastName }: WelcomeBannerProps) {
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setIsVisible(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
if (!isVisible) {
return null;
}
return (
<div className="text-sm font-mono px-2 p-1 fade-out-10 bg-green-500/10 text-green-900 dark:text-green-400">
Welcome,{" "}
<span className="font-semibold">
{firstName} {lastName}
</span>
</div>
);
}

View File

@ -15,6 +15,7 @@ export const formulaResultAtom = atom("");
export const deviceCartAtom = atom<Device[]>([]); export const deviceCartAtom = atom<Device[]>([]);
export const cartDrawerOpenAtom = atom(false); export const cartDrawerOpenAtom = atom(false);
export const WalletDrawerOpenAtom = atom(false); export const WalletDrawerOpenAtom = atom(false);
export const loadingDevicesToPayAtom = atom(false);
// Export the atoms with their store // Export the atoms with their store
export const atoms = { export const atoms = {
@ -27,4 +28,5 @@ export const atoms = {
deviceCartAtom, deviceCartAtom,
cartDrawerOpenAtom, cartDrawerOpenAtom,
walletTopUpValue, walletTopUpValue,
loadingDevicesToPayAtom,
}; };

10
package-lock.json generated
View File

@ -58,6 +58,7 @@
"eslint-config-next": "15.1.2", "eslint-config-next": "15.1.2",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"tailwindcss-motion": "^1.1.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
@ -7975,6 +7976,15 @@
"tailwindcss": ">=3.0.0 || insiders" "tailwindcss": ">=3.0.0 || insiders"
} }
}, },
"node_modules/tailwindcss-motion": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tailwindcss-motion/-/tailwindcss-motion-1.1.0.tgz",
"integrity": "sha512-0lK6rA4+367ffJdi1TtB72GlMCxJi2TP/xRiHc6An5pZSlU6WfIHhSvLxpcGilGZfBrOqc2q4woH1DEP/lCNbQ==",
"dev": true,
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.1",
"dev": true, "dev": true,

View File

@ -59,6 +59,7 @@
"eslint-config-next": "15.1.2", "eslint-config-next": "15.1.2",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"tailwindcss-motion": "^1.1.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },

View File

@ -1,6 +1,6 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate"; import tailwindcssAnimate from "tailwindcss-animate";
import tailwindcssMotion from "tailwindcss-motion";
export default { export default {
darkMode: ["class"], darkMode: ["class"],
content: [ content: [
@ -100,5 +100,5 @@ export default {
} }
} }
}, },
plugins: [tailwindcssAnimate], plugins: [tailwindcssAnimate, tailwindcssMotion],
} satisfies Config; } satisfies Config;