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";
if (isPlatinum)
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";
};
@ -64,25 +65,60 @@ export function InternetPlanCard({
return "default";
};
// Format plan name display to show just the plan tier prominently
const formatPlanName = () => {
if (plan.name) {
// Extract tier and offering type from name like "Internet Gold Plan (Home 1G)"
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>
);
}
const renderFeature = (feature: string, index: number) => {
const [label, detail] = feature.split(":");
if (detail) {
return (
<div className="text-lg font-semibold text-gray-900">
{plan.name}
</div>
<li key={index} className="flex items-start gap-2">
<span className="text-green-600 mt-0.5 flex-shrink-0"></span>
<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 (
@ -90,15 +126,24 @@ export function InternetPlanCard({
variant="static"
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 */}
<div className="flex items-start justify-between gap-4">
<div className="flex flex-col flex-1 min-w-0">
<div className="flex items-center gap-2">
{formatPlanName()}
{isGold && (
<CardBadge text="Recommended" variant="recommended" size="sm" />
)}
<div className="flex flex-col flex-1 min-w-0 gap-3">
<div className="flex items-center gap-2 flex-wrap text-sm">
<CardBadge text={plan.internetPlanTier ?? "Plan"} variant={getTierBadgeVariant()} size="sm" />
{isGold && <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>
@ -112,14 +157,9 @@ export function InternetPlanCard({
</div>
</div>
{/* Description */}
<p className="text-sm text-gray-600 leading-relaxed">
{plan.catalogMetadata?.tierDescription || plan.description}
</p>
{/* Features */}
<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">
{plan.catalogMetadata?.features && plan.catalogMetadata.features.length > 0 ? (
plan.catalogMetadata.features.map((feature, index) => (

View File

@ -2,8 +2,10 @@
import { PageLayout } from "@/components/templates/PageLayout";
import { ProgressSteps } from "@/components/molecules";
import { ServerIcon } from "@heroicons/react/24/outline";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { Button } from "@/components/atoms/button";
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 {
InternetPlanCatalogItem,
InternetInstallationCatalogItem,
@ -93,8 +95,6 @@ export function InternetConfigureContainer({
description="Set up your internet service options"
>
<div className="max-w-4xl mx-auto">
<CatalogBackLink href="/catalog/internet" label="Back to Internet Plans" />
{/* Plan Header */}
<PlanHeader plan={plan} />
@ -162,18 +162,36 @@ function PlanHeader({
}) {
return (
<div className="text-center mb-12">
<h2 className="text-2xl font-bold text-gray-900 mb-4">{plan.name}</h2>
<div className="inline-flex items-center gap-3 bg-gray-50 px-6 py-3 rounded-2xl border">
<span className="text-lg font-semibold text-blue-600">
¥{(plan.monthlyPrice ?? 0).toLocaleString()}
</span>
<span className="text-gray-400"></span>
<span className="text-sm text-gray-600">per month</span>
{plan.oneTimePrice && plan.oneTimePrice > 0 && (
<Button
as="a"
href="/catalog/internet"
variant="outline"
size="sm"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
className="group mb-6"
>
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>
<span className="text-sm text-gray-600">
¥{plan.oneTimePrice.toLocaleString()} setup
<CardBadge
text={plan.internetPlanTier}
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>
</>
)}
@ -181,3 +199,18 @@ function PlanHeader({
</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 { Button } from "@/components/atoms/button";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import type { ReactNode } from "react";
import type { InternetPlanCatalogItem } from "@customer-portal/domain/catalog";
import type { AccessMode } from "../../../../hooks/useConfigureParams";
@ -34,21 +34,27 @@ export function ServiceConfigurationStep({ plan, mode, setMode, isTransitioning,
</div>
{plan?.internetPlanTier === "Platinum" && (
<AlertBanner
variant="warning"
title="IMPORTANT - For PLATINUM subscribers"
className="mb-6"
elevated
>
<p>
Additional fees are incurred for the PLATINUM service. Please refer to the information
from our tech team for details.
</p>
<p className="text-xs mt-2">
* Will appear on the invoice as &quot;Platinum Base Plan&quot;. Device subscriptions
will be added later.
</p>
</AlertBanner>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-yellow-600 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path
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"
clipRule="evenodd"
/>
</svg>
<div>
<h4 className="font-medium text-yellow-900">IMPORTANT - For PLATINUM subscribers</h4>
<p className="text-sm text-yellow-800 mt-1">
Additional fees are incurred for the PLATINUM service. Please refer to the information
from our tech team for details.
</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" ? (
@ -85,17 +91,31 @@ function SilverPlanConfiguration({
mode="PPPoE"
selectedMode={mode}
onSelect={setMode}
title="PPPoE"
description="Point-to-Point Protocol over Ethernet"
details="Traditional connection method with username/password authentication. Compatible with most routers and ISPs."
title="Any Router + PPPoE"
description="Works with most routers you already own or can purchase anywhere."
note="PPPoE may experience network congestion during peak hours, potentially resulting in slower speeds."
tone="warning"
/>
<ModeSelectionCard
mode="IPoE-BYOR"
selectedMode={mode}
onSelect={setMode}
title="IPoE (BYOR)"
description="IP over Ethernet"
details="Modern connection method with automatic configuration. Simplified setup with faster connection times."
title="v6plus Router + IPoE"
description="Requires a v6plus-compatible router for faster, more stable connection."
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>
@ -108,24 +128,31 @@ function ModeSelectionCard({
onSelect,
title,
description,
details,
note,
tone,
}: {
mode: AccessMode;
selectedMode: AccessMode | null;
onSelect: (mode: AccessMode) => void;
title: string;
description: string;
details: string;
note: ReactNode;
tone: "warning" | "success";
}) {
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 (
<button
type="button"
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
? "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"
}`}
aria-pressed={isSelected}
@ -145,29 +172,28 @@ function ModeSelectionCard({
</div>
</div>
<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>
);
}
function StandardPlanConfiguration({ plan }: { plan: InternetPlanCatalogItem }) {
return (
<div className="bg-gray-50 rounded-lg p-6">
<h4 className="font-medium text-gray-900 mb-4">Plan Details</h4>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600">Plan Name:</span>
<span className="font-medium">{plan.name}</span>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-green-600 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
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 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>
);