- Introduced PortaledNotificationBell component to render the NotificationBell in the PageLayout header, ensuring it remains persistent across page navigations. - Updated AppShell to utilize the new portaled NotificationBell, enhancing the layout and user interaction. - Adjusted PageLayout to accommodate the new notification rendering logic, improving overall UI consistency. - Made minor adjustments to various components for better alignment and spacing.
127 lines
4.1 KiB
TypeScript
127 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
CreditCardIcon,
|
|
BanknotesIcon,
|
|
DevicePhoneMobileIcon,
|
|
CheckCircleIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import type { PaymentMethod } from "@customer-portal/domain/payments";
|
|
import {
|
|
cn,
|
|
getPaymentMethodBrandLabel,
|
|
getPaymentMethodCardDisplay,
|
|
normalizeExpiryLabel,
|
|
} from "@/shared/utils";
|
|
import type { ReactNode } from "react";
|
|
|
|
interface PaymentMethodCardProps {
|
|
paymentMethod: PaymentMethod;
|
|
className?: string;
|
|
showActions?: boolean;
|
|
actionSlot?: ReactNode;
|
|
}
|
|
|
|
const getBrandColor = (brand?: string) => {
|
|
const brandLower = brand?.toLowerCase() || "";
|
|
|
|
if (brandLower.includes("visa")) return "from-blue-600 to-blue-700";
|
|
if (brandLower.includes("mastercard") || brandLower.includes("master"))
|
|
return "from-red-500 to-red-600";
|
|
if (brandLower.includes("amex") || brandLower.includes("american"))
|
|
return "from-gray-700 to-gray-800";
|
|
if (brandLower.includes("discover")) return "from-orange-500 to-orange-600";
|
|
if (brandLower.includes("mobile")) return "from-purple-500 to-purple-600";
|
|
|
|
return "from-gray-500 to-gray-600"; // Default
|
|
};
|
|
|
|
const getMethodIcon = (type: PaymentMethod["type"], brand?: string) => {
|
|
const baseClasses =
|
|
"w-9 h-9 bg-gradient-to-br rounded-lg flex items-center justify-center shadow-sm";
|
|
|
|
if (isBankAccount(type)) {
|
|
return (
|
|
<div className={`${baseClasses} from-green-500 to-green-600`}>
|
|
<BanknotesIcon className="h-[18px] w-[18px] text-white" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const brandColor = getBrandColor(brand);
|
|
const IconComponent = brand?.toLowerCase().includes("mobile")
|
|
? DevicePhoneMobileIcon
|
|
: CreditCardIcon;
|
|
|
|
return (
|
|
<div className={`${baseClasses} ${brandColor}`}>
|
|
<IconComponent className="h-[18px] w-[18px] text-white" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const isBankAccount = (type: PaymentMethod["type"]) =>
|
|
type === "BankAccount" || type === "RemoteBankAccount";
|
|
|
|
const formatExpiry = (expiryDate?: string) => {
|
|
const normalized = normalizeExpiryLabel(expiryDate);
|
|
return normalized ? `Expires ${normalized}` : null;
|
|
};
|
|
|
|
export function PaymentMethodCard({
|
|
paymentMethod,
|
|
className,
|
|
showActions = false,
|
|
actionSlot,
|
|
}: PaymentMethodCardProps) {
|
|
const cardDisplay = getPaymentMethodCardDisplay(paymentMethod);
|
|
const cardBrand = getPaymentMethodBrandLabel(paymentMethod);
|
|
const expiry = formatExpiry(paymentMethod.expiryDate);
|
|
const icon = getMethodIcon(paymentMethod.type, paymentMethod.cardType);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex items-center justify-between p-4 border border-gray-200 rounded-lg bg-white transition-all duration-200 hover:shadow-sm",
|
|
paymentMethod.isDefault && "ring-2 ring-blue-500/20 border-blue-200 bg-blue-50/30",
|
|
className
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3.5 flex-1 min-w-0">
|
|
<div className="flex-shrink-0">{icon}</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2.5 mb-0.5">
|
|
<h3 className="font-semibold text-gray-900 text-sm font-mono">{cardDisplay}</h3>
|
|
{paymentMethod.isDefault && (
|
|
<div className="flex items-center gap-1 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-1 rounded-full">
|
|
<CheckCircleIcon className="h-3 w-3" />
|
|
Default
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 text-xs">
|
|
{cardBrand && <span className="text-gray-600 font-medium">{cardBrand}</span>}
|
|
{expiry && (
|
|
<>
|
|
{cardBrand && <span className="text-gray-300">•</span>}
|
|
<span className="text-gray-500">{expiry}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{paymentMethod.isDefault && (
|
|
<div className="text-xs text-blue-600 font-medium mt-1">
|
|
This card will be used for automatic payments
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{showActions && actionSlot && <div className="flex-shrink-0 ml-4">{actionSlot}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export type { PaymentMethodCardProps };
|