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:
parent
aaabb795c1
commit
31bd4ba8c6
@ -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) => (
|
||||||
|
|||||||
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 "Platinum Base Plan". 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>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user