barsa 4ee9cb526b feat: implement portaled NotificationBell in AppShell for improved user experience
- 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.
2026-03-06 15:42:25 +09:00

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 };