Refactor InternetPlanCard and InternetConfigureContainer for improved UI and feature display

- Enhanced InternetPlanCard to better format plan names and display features dynamically.
- Updated InternetConfigureContainer to include a back button and improved plan header presentation.
- Refactored ServiceConfigurationStep to enhance the display of important information for Platinum subscribers and improve access mode selection.
- Streamlined styling and layout across components for better visual consistency and user experience.
This commit is contained in:
barsa 2025-10-22 18:00:54 +09:00
parent aaabb795c1
commit 31bd4ba8c6
3 changed files with 186 additions and 87 deletions

View File

@ -53,7 +53,8 @@ export function InternetPlanCard({
return "border border-yellow-200 bg-white shadow-lg hover:shadow-xl ring-1 ring-yellow-100"; return "border border-yellow-200 bg-white shadow-lg hover:shadow-xl ring-1 ring-yellow-100";
if (isPlatinum) if (isPlatinum)
return "border border-indigo-200 bg-white shadow-lg hover:shadow-xl ring-1 ring-indigo-100"; return "border border-indigo-200 bg-white shadow-lg hover:shadow-xl ring-1 ring-indigo-100";
if (isSilver) return "border border-gray-200 bg-white shadow-lg hover:shadow-xl ring-1 ring-gray-100"; if (isSilver)
return "border border-gray-200 bg-white shadow-lg hover:shadow-xl ring-1 ring-gray-100";
return "border border-gray-200 bg-white shadow hover:shadow-lg"; return "border border-gray-200 bg-white shadow hover:shadow-lg";
}; };
@ -64,25 +65,60 @@ export function InternetPlanCard({
return "default"; return "default";
}; };
// Format plan name display to show just the plan tier prominently const renderFeature = (feature: string, index: number) => {
const formatPlanName = () => { const [label, detail] = feature.split(":");
if (plan.name) {
// Extract tier and offering type from name like "Internet Gold Plan (Home 1G)" if (detail) {
const match = plan.name.match(/(\w+)\s+Plan\s+\((.*?)\)/);
if (match) {
return (
<div className="text-base font-semibold text-gray-900 leading-tight">
{match[1]} Plan ({match[2]})
</div>
);
}
return ( return (
<div className="text-lg font-semibold text-gray-900"> <li key={index} className="flex items-start gap-2">
{plan.name} <span className="text-green-600 mt-0.5 flex-shrink-0"></span>
</div> <span>
<span className="font-medium text-gray-900">{label.trim()}:</span>{" "}
<span className="text-gray-700">{detail.trim()}</span>
</span>
</li>
); );
} }
return <h3 className="text-xl font-semibold text-gray-900 leading-tight">{plan.name}</h3>;
return (
<li key={index} className="flex items-start gap-2">
<span className="text-green-600 mt-0.5 flex-shrink-0"></span>
<span className="text-gray-700">{feature}</span>
</li>
);
};
const renderPlanFeatures = () => {
if (plan.catalogMetadata?.features && plan.catalogMetadata.features.length > 0) {
return plan.catalogMetadata.features.map(renderFeature);
}
const priceSummaryParts = [
typeof plan.monthlyPrice === "number" && plan.monthlyPrice > 0
? `Monthly: ¥${plan.monthlyPrice.toLocaleString()}`
: null,
typeof plan.oneTimePrice === "number" && plan.oneTimePrice > 0
? `One-time: ¥${plan.oneTimePrice.toLocaleString()}`
: null,
].filter(Boolean) as string[];
const fallbackFeatures = [
"NTT Optical Fiber (Flet's Hikari Next)",
`${plan.internetOfferingType?.includes("Apartment") ? "Mansion" : "Home"} ${
plan.internetOfferingType?.includes("10G")
? "10Gbps"
: plan.internetOfferingType?.includes("100M")
? "100Mbps"
: "1Gbps"
} connection`,
"ISP connection protocols: IPoE and PPPoE",
...(priceSummaryParts.length > 0 ? [priceSummaryParts.join(" | ")] : []),
installations.length > 0 && minInstallationPrice > 0
? `Installation from ¥${minInstallationPrice.toLocaleString()}`
: null,
].filter((Boolean)) as string[];
return fallbackFeatures.map(renderFeature);
}; };
return ( return (
@ -90,15 +126,24 @@ export function InternetPlanCard({
variant="static" variant="static"
className={`overflow-hidden flex flex-col h-full transition-all duration-300 ease-out hover:-translate-y-1 ${getBorderClass()}`} className={`overflow-hidden flex flex-col h-full transition-all duration-300 ease-out hover:-translate-y-1 ${getBorderClass()}`}
> >
<div className="p-6 flex flex-col flex-grow space-y-4"> <div className="p-6 flex flex-col flex-grow space-y-5">
{/* Header with badges and pricing */} {/* Header with badges and pricing */}
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex flex-col flex-1 min-w-0"> <div className="flex flex-col flex-1 min-w-0 gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-wrap text-sm">
{formatPlanName()} <CardBadge text={plan.internetPlanTier ?? "Plan"} variant={getTierBadgeVariant()} size="sm" />
{isGold && ( {isGold && <CardBadge text="Recommended" variant="recommended" size="sm" />}
<CardBadge text="Recommended" variant="recommended" size="sm" /> </div>
)}
<div>
<h3 className="text-xl font-semibold text-gray-900 leading-tight break-words">
{plan.name}
</h3>
{plan.catalogMetadata?.tierDescription || plan.description ? (
<p className="mt-1 text-sm text-gray-600 leading-relaxed">
{plan.catalogMetadata?.tierDescription || plan.description}
</p>
) : null}
</div> </div>
</div> </div>
@ -112,14 +157,9 @@ export function InternetPlanCard({
</div> </div>
</div> </div>
{/* Description */}
<p className="text-sm text-gray-600 leading-relaxed">
{plan.catalogMetadata?.tierDescription || plan.description}
</p>
{/* Features */} {/* Features */}
<div className="flex-grow"> <div className="flex-grow">
<h4 className="font-medium text-gray-900 mb-3 text-sm">Plan Includes:</h4> <h4 className="font-medium text-gray-900 mb-3 text-sm">Your Plan Includes:</h4>
<ul className="space-y-2 text-sm text-gray-700"> <ul className="space-y-2 text-sm text-gray-700">
{plan.catalogMetadata?.features && plan.catalogMetadata.features.length > 0 ? ( {plan.catalogMetadata?.features && plan.catalogMetadata.features.length > 0 ? (
plan.catalogMetadata.features.map((feature, index) => ( plan.catalogMetadata.features.map((feature, index) => (

View File

@ -2,8 +2,10 @@
import { PageLayout } from "@/components/templates/PageLayout"; import { PageLayout } from "@/components/templates/PageLayout";
import { ProgressSteps } from "@/components/molecules"; import { ProgressSteps } from "@/components/molecules";
import { ServerIcon } from "@heroicons/react/24/outline"; import { Button } from "@/components/atoms/button";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink"; import { CardBadge } from "@/features/catalog/components/base/CardBadge";
import type { BadgeVariant } from "@/features/catalog/components/base/CardBadge";
import { ServerIcon, ArrowLeftIcon } from "@heroicons/react/24/outline";
import type { import type {
InternetPlanCatalogItem, InternetPlanCatalogItem,
InternetInstallationCatalogItem, InternetInstallationCatalogItem,
@ -93,8 +95,6 @@ export function InternetConfigureContainer({
description="Set up your internet service options" description="Set up your internet service options"
> >
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<CatalogBackLink href="/catalog/internet" label="Back to Internet Plans" />
{/* Plan Header */} {/* Plan Header */}
<PlanHeader plan={plan} /> <PlanHeader plan={plan} />
@ -162,18 +162,36 @@ function PlanHeader({
}) { }) {
return ( return (
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-2xl font-bold text-gray-900 mb-4">{plan.name}</h2> <Button
<div className="inline-flex items-center gap-3 bg-gray-50 px-6 py-3 rounded-2xl border"> as="a"
<span className="text-lg font-semibold text-blue-600"> href="/catalog/internet"
¥{(plan.monthlyPrice ?? 0).toLocaleString()} variant="outline"
</span> size="sm"
<span className="text-gray-400"></span> leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
<span className="text-sm text-gray-600">per month</span> className="group mb-6"
{plan.oneTimePrice && plan.oneTimePrice > 0 && ( >
Back to Internet Plans
</Button>
<h1 className="text-3xl font-bold text-gray-900 mb-4">Configure {plan.name}</h1>
<div className="inline-flex items-center gap-3 bg-gray-50 px-6 py-3 rounded-2xl border border-gray-200">
{plan.internetPlanTier && (
<> <>
<span className="text-gray-400"></span> <CardBadge
<span className="text-sm text-gray-600"> text={plan.internetPlanTier}
¥{plan.oneTimePrice.toLocaleString()} setup variant={getTierBadgeVariant(plan.internetPlanTier)}
size="md"
/>
<span className="text-gray-500"></span>
</>
)}
<span className="font-medium text-gray-900">{plan.name}</span>
{plan.monthlyPrice && plan.monthlyPrice > 0 && (
<>
<span className="text-gray-500"></span>
<span className="font-semibold text-gray-900">
¥{plan.monthlyPrice.toLocaleString()}/month
</span> </span>
</> </>
)} )}
@ -181,3 +199,18 @@ function PlanHeader({
</div> </div>
); );
} }
function getTierBadgeVariant(tier?: string | null): BadgeVariant {
switch (tier) {
case "Gold":
return "gold";
case "Platinum":
return "platinum";
case "Silver":
return "silver";
case "Recommended":
return "recommended";
default:
return "default";
}
}

View File

@ -2,8 +2,8 @@
import { AnimatedCard } from "@/components/molecules"; import { AnimatedCard } from "@/components/molecules";
import { Button } from "@/components/atoms/button"; import { Button } from "@/components/atoms/button";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { ArrowRightIcon } from "@heroicons/react/24/outline"; import { ArrowRightIcon } from "@heroicons/react/24/outline";
import type { ReactNode } from "react";
import type { InternetPlanCatalogItem } from "@customer-portal/domain/catalog"; import type { InternetPlanCatalogItem } from "@customer-portal/domain/catalog";
import type { AccessMode } from "../../../../hooks/useConfigureParams"; import type { AccessMode } from "../../../../hooks/useConfigureParams";
@ -34,21 +34,27 @@ export function ServiceConfigurationStep({ plan, mode, setMode, isTransitioning,
</div> </div>
{plan?.internetPlanTier === "Platinum" && ( {plan?.internetPlanTier === "Platinum" && (
<AlertBanner <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
variant="warning" <div className="flex items-start gap-3">
title="IMPORTANT - For PLATINUM subscribers" <svg className="w-5 h-5 text-yellow-600 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
className="mb-6" <path
elevated fillRule="evenodd"
> d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
<p> clipRule="evenodd"
Additional fees are incurred for the PLATINUM service. Please refer to the information />
from our tech team for details. </svg>
</p> <div>
<p className="text-xs mt-2"> <h4 className="font-medium text-yellow-900">IMPORTANT - For PLATINUM subscribers</h4>
* Will appear on the invoice as &quot;Platinum Base Plan&quot;. Device subscriptions <p className="text-sm text-yellow-800 mt-1">
will be added later. Additional fees are incurred for the PLATINUM service. Please refer to the information
</p> from our tech team for details.
</AlertBanner> </p>
<p className="text-xs text-yellow-700 mt-2">
* Will appear on the invoice as "Platinum Base Plan". Device subscriptions will be added later.
</p>
</div>
</div>
</div>
)} )}
{plan?.internetPlanTier === "Silver" ? ( {plan?.internetPlanTier === "Silver" ? (
@ -85,17 +91,31 @@ function SilverPlanConfiguration({
mode="PPPoE" mode="PPPoE"
selectedMode={mode} selectedMode={mode}
onSelect={setMode} onSelect={setMode}
title="PPPoE" title="Any Router + PPPoE"
description="Point-to-Point Protocol over Ethernet" description="Works with most routers you already own or can purchase anywhere."
details="Traditional connection method with username/password authentication. Compatible with most routers and ISPs." note="PPPoE may experience network congestion during peak hours, potentially resulting in slower speeds."
tone="warning"
/> />
<ModeSelectionCard <ModeSelectionCard
mode="IPoE-BYOR" mode="IPoE-BYOR"
selectedMode={mode} selectedMode={mode}
onSelect={setMode} onSelect={setMode}
title="IPoE (BYOR)" title="v6plus Router + IPoE"
description="IP over Ethernet" description="Requires a v6plus-compatible router for faster, more stable connection."
details="Modern connection method with automatic configuration. Simplified setup with faster connection times." note={
<span>
<strong>Recommended:</strong> Faster speeds with less congestion.{" "}
<a
href="https://www.jpix.ad.jp/service/?p=3565"
target="_blank"
rel="noreferrer"
className="text-blue-600 underline"
>
Check compatibility
</a>
</span>
}
tone="success"
/> />
</div> </div>
</div> </div>
@ -108,24 +128,31 @@ function ModeSelectionCard({
onSelect, onSelect,
title, title,
description, description,
details, note,
tone,
}: { }: {
mode: AccessMode; mode: AccessMode;
selectedMode: AccessMode | null; selectedMode: AccessMode | null;
onSelect: (mode: AccessMode) => void; onSelect: (mode: AccessMode) => void;
title: string; title: string;
description: string; description: string;
details: string; note: ReactNode;
tone: "warning" | "success";
}) { }) {
const isSelected = selectedMode === mode; const isSelected = selectedMode === mode;
const toneClasses =
tone === "warning"
? "bg-orange-100 text-orange-800 border-orange-200"
: "bg-green-100 text-green-800 border-green-200";
return ( return (
<button <button
type="button" type="button"
onClick={() => onSelect(mode)} onClick={() => onSelect(mode)}
className={`p-6 rounded-xl border-2 text-left transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${ className={`p-6 rounded-xl border-2 text-left transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
isSelected isSelected
? "border-blue-500 bg-blue-50 shadow-md" ? "border-blue-500 bg-blue-50 shadow-md scale-[1.02]"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50" : "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
}`} }`}
aria-pressed={isSelected} aria-pressed={isSelected}
@ -145,29 +172,28 @@ function ModeSelectionCard({
</div> </div>
</div> </div>
<p className="text-sm text-gray-600 mb-2">{description}</p> <p className="text-sm text-gray-600 mb-2">{description}</p>
<p className="text-xs text-gray-500">{details}</p> <div className={`rounded-lg border px-3 py-2 text-xs ${toneClasses}`}>{note}</div>
</button> </button>
); );
} }
function StandardPlanConfiguration({ plan }: { plan: InternetPlanCatalogItem }) { function StandardPlanConfiguration({ plan }: { plan: InternetPlanCatalogItem }) {
return ( return (
<div className="bg-gray-50 rounded-lg p-6"> <div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-4">Plan Details</h4> <div className="flex items-start gap-2">
<div className="space-y-3"> <svg className="w-5 h-5 text-green-600 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<div className="flex justify-between"> <path
<span className="text-gray-600">Plan Name:</span> fillRule="evenodd"
<span className="font-medium">{plan.name}</span> d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
<div>
<h4 className="font-medium text-green-900">Access Mode Pre-configured</h4>
<p className="text-sm text-green-800 mt-1">
Access Mode: IPoE-HGW (Pre-configured for {plan.internetPlanTier} plan)
</p>
</div> </div>
<div className="flex justify-between">
<span className="text-gray-600">Tier:</span>
<span className="font-medium">{plan.internetPlanTier}</span>
</div>
{plan.description && (
<div className="pt-3 border-t border-gray-200">
<p className="text-sm text-gray-600">{plan.description}</p>
</div>
)}
</div> </div>
</div> </div>
); );