From 47414f10e04be497d51c8f90b700be89158265db Mon Sep 17 00:00:00 2001 From: barsa Date: Wed, 24 Dec 2025 14:58:56 +0900 Subject: [PATCH] Enhance Internet Offering Card and Public Internet Views - Integrated InternetTierPricingModal into InternetOfferingCard for improved pricing visibility in preview mode. - Updated pricing display logic to conditionally show pricing based on preview mode. - Refined feature display in InternetOfferingCard to enhance user experience. - Revamped PublicInternetConfigure and PublicInternetPlans views for a cleaner, more focused signup flow and improved FAQ section. - Streamlined offering card presentation and added dynamic FAQ item expansion for better user engagement. --- .../internet/InternetModalShell.tsx | 141 ++++ .../internet/InternetOfferingCard.tsx | 102 ++- .../internet/InternetTierPricingModal.tsx | 120 +++ .../internet/PublicOfferingCard.tsx | 272 +++++++ .../internet/WhyChooseUsPillars.tsx | 106 +++ .../catalog/views/PublicInternetConfigure.tsx | 181 ++--- .../catalog/views/PublicInternetPlans.tsx | 704 +++++++++--------- 7 files changed, 1142 insertions(+), 484 deletions(-) create mode 100644 apps/portal/src/features/catalog/components/internet/InternetModalShell.tsx create mode 100644 apps/portal/src/features/catalog/components/internet/InternetTierPricingModal.tsx create mode 100644 apps/portal/src/features/catalog/components/internet/PublicOfferingCard.tsx create mode 100644 apps/portal/src/features/catalog/components/internet/WhyChooseUsPillars.tsx diff --git a/apps/portal/src/features/catalog/components/internet/InternetModalShell.tsx b/apps/portal/src/features/catalog/components/internet/InternetModalShell.tsx new file mode 100644 index 00000000..f8d39ccd --- /dev/null +++ b/apps/portal/src/features/catalog/components/internet/InternetModalShell.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useEffect, useId, useRef } from "react"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import { cn } from "@/lib/utils"; + +interface InternetModalShellProps { + isOpen: boolean; + onClose: () => void; + title: string; + description?: string; + children: React.ReactNode; + size?: "md" | "lg"; +} + +const sizeMap: Record, string> = { + md: "max-w-lg", + lg: "max-w-3xl", +}; + +/** + * Lightweight modal shell (overlay + card) used by the Internet shop experience. + * Implements: + * - Backdrop click to close + * - Escape to close + * - Simple focus trap + focus restore (pattern aligned with SessionTimeoutWarning) + */ +export function InternetModalShell({ + isOpen, + onClose, + title, + description, + children, + size = "lg", +}: InternetModalShellProps) { + const titleId = useId(); + const descriptionId = useId(); + const dialogRef = useRef(null); + const previouslyFocusedElement = useRef(null); + + useEffect(() => { + if (!isOpen) return; + + previouslyFocusedElement.current = document.activeElement as HTMLElement | null; + + const focusTimer = window.setTimeout(() => { + dialogRef.current?.focus(); + }, 0); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + onClose(); + } + + if (event.key === "Tab") { + const focusableElements = dialogRef.current?.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + + if (!focusableElements || focusableElements.length === 0) { + event.preventDefault(); + return; + } + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + if (!event.shiftKey && document.activeElement === lastElement) { + event.preventDefault(); + firstElement.focus(); + } + + if (event.shiftKey && document.activeElement === firstElement) { + event.preventDefault(); + lastElement.focus(); + } + } + }; + + document.addEventListener("keydown", handleKeyDown); + + return () => { + clearTimeout(focusTimer); + document.removeEventListener("keydown", handleKeyDown); + previouslyFocusedElement.current?.focus(); + }; + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > + + ); +} diff --git a/apps/portal/src/features/catalog/components/internet/InternetOfferingCard.tsx b/apps/portal/src/features/catalog/components/internet/InternetOfferingCard.tsx index 3695eefa..c2983fbf 100644 --- a/apps/portal/src/features/catalog/components/internet/InternetOfferingCard.tsx +++ b/apps/portal/src/features/catalog/components/internet/InternetOfferingCard.tsx @@ -11,6 +11,7 @@ import { import { Button } from "@/components/atoms/button"; import { CardBadge } from "@/features/catalog/components/base/CardBadge"; import { cn } from "@/lib/utils"; +import { InternetTierPricingModal } from "@/features/catalog/components/internet/InternetTierPricingModal"; interface TierInfo { tier: "Silver" | "Gold" | "Platinum"; @@ -84,6 +85,7 @@ export function InternetOfferingCard({ previewMode = false, }: InternetOfferingCardProps) { const [isExpanded, setIsExpanded] = useState(defaultExpanded); + const [pricingOpen, setPricingOpen] = useState(false); const Icon = iconType === "home" ? HomeIcon : BuildingOfficeIcon; @@ -135,7 +137,13 @@ export function InternetOfferingCard({
- {isExpanded ? "Hide plans" : "View plans"} + {previewMode + ? isExpanded + ? "Hide tiers" + : "Preview tiers" + : isExpanded + ? "Hide plans" + : "View plans"} {isExpanded ? ( @@ -161,44 +169,48 @@ export function InternetOfferingCard({ {/* Header */}
{tier.tier} - {tier.recommended && ( + {tier.recommended ? ( - )} + ) : null}
- {/* Pricing */} -
-
- - ¥{tier.monthlyPrice.toLocaleString()} - - /mo + {/* Pricing (hidden in preview mode) */} + {!previewMode ? ( +
+
+ + ¥{tier.monthlyPrice.toLocaleString()} + + /mo +
+ {tier.pricingNote ? ( +

{tier.pricingNote}

+ ) : null}
- {tier.pricingNote && ( -

{tier.pricingNote}

- )} -
+ ) : null} {/* Description */}

{tier.description}

- {/* Features - flex-grow to push button to bottom */} -
    - {tier.features.map((feature, index) => ( -
  • - - - {feature} - -
  • - ))} + {/* Features */} +
      + {(previewMode ? tier.features.slice(0, 3) : tier.features).map( + (feature, index) => ( +
    • + + + {feature} + +
    • + ) + )}
    {/* Button/Info - always at bottom */} {previewMode ? (

    - Available after verification + Prices shown after you click “See pricing”

    ) : disabled ? ( @@ -206,11 +218,11 @@ export function InternetOfferingCard({ - {disabledReason && ( + {disabledReason ? (

    {disabledReason}

    - )} + ) : null}
) : ( + +
+
+ ) : ( +

+ + ¥{setupFee.toLocaleString()} one-time installation (or 12/24-month installment) +

+ )} )} + + {/* Pricing modal (public preview mode only) */} + {previewMode ? ( + setPricingOpen(false)} + offeringTitle={title} + offeringSubtitle={`${speedBadge}${isPremium ? " · select areas" : ""}`} + tiers={tiers} + setupFee={setupFee} + ctaHref={ctaPath} + /> + ) : null} ); } diff --git a/apps/portal/src/features/catalog/components/internet/InternetTierPricingModal.tsx b/apps/portal/src/features/catalog/components/internet/InternetTierPricingModal.tsx new file mode 100644 index 00000000..d97af70c --- /dev/null +++ b/apps/portal/src/features/catalog/components/internet/InternetTierPricingModal.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { BoltIcon } from "@heroicons/react/24/outline"; +import { Button } from "@/components/atoms/button"; +import { CardBadge } from "@/features/catalog/components/base/CardBadge"; +import { cn } from "@/lib/utils"; +import type { TierInfo } from "@/features/catalog/components/internet/InternetOfferingCard"; +import { InternetModalShell } from "@/features/catalog/components/internet/InternetModalShell"; + +interface InternetTierPricingModalProps { + isOpen: boolean; + onClose: () => void; + offeringTitle: string; + offeringSubtitle?: string; + tiers: TierInfo[]; + setupFee: number; + ctaHref: string; +} + +const tierStyles = { + Silver: { + card: "border-muted-foreground/20 bg-card", + accent: "text-muted-foreground", + }, + Gold: { + card: "border-warning/30 bg-warning-soft/20", + accent: "text-warning", + }, + Platinum: { + card: "border-primary/30 bg-info-soft/20", + accent: "text-primary", + }, +} as const; + +export function InternetTierPricingModal({ + isOpen, + onClose, + offeringTitle, + offeringSubtitle, + tiers, + setupFee, + ctaHref, +}: InternetTierPricingModalProps) { + return ( + +
+ {offeringSubtitle ? ( +
+
{offeringTitle}
+
{offeringSubtitle}
+
+ ) : null} + +
+ {tiers.map(tier => ( +
+
+ {tier.tier} + {tier.recommended ? ( + + ) : null} +
+ +
+
+ + ¥{tier.monthlyPrice.toLocaleString()} + + /mo +
+ {tier.pricingNote ? ( +

{tier.pricingNote}

+ ) : null} +
+ +

{tier.description}

+ +
    + {tier.features.map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+ +
+

+ + ¥{setupFee.toLocaleString()} setup +

+
+
+ ))} +
+ +
+ + +
+
+
+ ); +} diff --git a/apps/portal/src/features/catalog/components/internet/PublicOfferingCard.tsx b/apps/portal/src/features/catalog/components/internet/PublicOfferingCard.tsx new file mode 100644 index 00000000..e67a0dda --- /dev/null +++ b/apps/portal/src/features/catalog/components/internet/PublicOfferingCard.tsx @@ -0,0 +1,272 @@ +"use client"; + +import { useState } from "react"; +import { + ChevronDownIcon, + ChevronUpIcon, + HomeIcon, + BuildingOfficeIcon, + BoltIcon, + InformationCircleIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +import { Button } from "@/components/atoms/button"; +import { CardBadge } from "@/features/catalog/components/base/CardBadge"; +import { cn } from "@/lib/utils"; + +interface TierInfo { + tier: "Silver" | "Gold" | "Platinum"; + monthlyPrice: number; + description: string; + features: string[]; + pricingNote?: string; +} + +interface PublicOfferingCardProps { + offeringType: string; + title: string; + speedBadge: string; + description: string; + iconType: "home" | "apartment"; + startingPrice: number; + setupFee: number; + tiers: TierInfo[]; + isPremium?: boolean; + ctaPath: string; + defaultExpanded?: boolean; + /** Show info tooltip explaining connection types (for Apartment) */ + showConnectionInfo?: boolean; +} + +const tierStyles = { + Silver: { + card: "border-muted-foreground/20 bg-card", + accent: "text-muted-foreground", + }, + Gold: { + card: "border-warning/30 bg-warning-soft/20", + accent: "text-warning", + }, + Platinum: { + card: "border-primary/30 bg-info-soft/20", + accent: "text-primary", + }, +} as const; + +/** + * Info panel explaining apartment connection types + */ +function ConnectionTypeInfo({ onClose }: { onClose: () => void }) { + return ( +
+
+
+ +

+ Why does speed vary by building? +

+
+ +
+
+

+ Apartment buildings in Japan have different fiber infrastructure installed by NTT. Your + available speed depends on what your building supports: +

+
+
+ FTTH (1Gbps) + + — Fiber directly to your unit. Fastest option, available in newer buildings. + +
+
+ VDSL (100Mbps) + + — Fiber to building, then phone line to your unit. Most common in older buildings. + +
+
+ LAN (100Mbps) + + — Fiber to building, then ethernet to your unit. Common in some mansion types. + +
+
+

+ Good news: All types have the same monthly price (¥4,800~). We'll check what's + available at your address. +

+
+
+ ); +} + +/** + * Public-facing offering card that shows pricing inline + * No modals - all information is visible or expandable within the card + */ +export function PublicOfferingCard({ + title, + speedBadge, + description, + iconType, + startingPrice, + setupFee, + tiers, + isPremium = false, + ctaPath, + defaultExpanded = false, + showConnectionInfo = false, +}: PublicOfferingCardProps) { + const [isExpanded, setIsExpanded] = useState(defaultExpanded); + const [showInfo, setShowInfo] = useState(false); + + const Icon = iconType === "home" ? HomeIcon : BuildingOfficeIcon; + + return ( +
+ {/* Header - Always visible */} + + + {/* Expanded content - Tier pricing shown inline */} + {isExpanded && ( +
+ {/* Connection type info button (for Apartment) */} + {showConnectionInfo && !showInfo && ( + + )} + + {/* Connection type info panel */} + {showConnectionInfo && showInfo && ( + setShowInfo(false)} /> + )} + + {/* Tier cards - 3 columns on desktop */} +
+ {tiers.map(tier => ( +
+ {/* Header */} +
+ + {tier.tier} + +
+ + {/* Price - Always visible */} +
+
+ + ¥{tier.monthlyPrice.toLocaleString()} + + /mo + {tier.pricingNote && ( + {tier.pricingNote} + )} +
+
+ + {/* Description */} +

{tier.description}

+ + {/* Features */} +
    + {tier.features.slice(0, 3).map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+
+ ))} +
+ + {/* Footer with setup fee and CTA */} +
+

+ + + ¥{setupFee.toLocaleString()} one-time setup + {" "} + (or 12/24-month installment) +

+ +
+
+ )} +
+ ); +} + +export type { PublicOfferingCardProps, TierInfo }; diff --git a/apps/portal/src/features/catalog/components/internet/WhyChooseUsPillars.tsx b/apps/portal/src/features/catalog/components/internet/WhyChooseUsPillars.tsx new file mode 100644 index 00000000..f3ac3aa6 --- /dev/null +++ b/apps/portal/src/features/catalog/components/internet/WhyChooseUsPillars.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { + WifiIcon, + ChatBubbleLeftRightIcon, + BoltIcon, + WrenchScrewdriverIcon, + DocumentTextIcon, + GlobeAltIcon, +} from "@heroicons/react/24/outline"; +import { CheckCircleIcon } from "@heroicons/react/24/solid"; + +interface FeatureProps { + icon: React.ReactNode; + title: string; + description: string; + highlight?: string; +} + +function FeatureCard({ icon, title, description, highlight }: FeatureProps) { + return ( +
+
+ {icon} +
+
+

{title}

+

{description}

+ {highlight && ( + + + {highlight} + + )} +
+
+ ); +} + +/** + * Why Choose Us - Clean feature grid + * 6 key differentiators in a 3x2 grid on desktop, 2x3 on tablet, stacked on mobile + */ +export function WhyChooseUsPillars() { + const features: FeatureProps[] = [ + { + icon: , + title: "NTT Optical Fiber", + description: "Japan's most reliable network with speeds up to 10Gbps", + highlight: "99.9% uptime", + }, + { + icon: , + title: "IPv6/IPoE Ready", + description: "Next-gen protocol for congestion-free browsing", + highlight: "No peak-hour slowdowns", + }, + { + icon: , + title: "Full English Support", + description: "Native English service for setup, billing & technical help", + highlight: "No language barriers", + }, + { + icon: , + title: "One Bill, One Provider", + description: "NTT line + ISP + equipment bundled with simple billing", + highlight: "No hidden fees", + }, + { + icon: , + title: "On-site Support", + description: "Technicians can visit for installation & troubleshooting", + highlight: "Professional setup", + }, + { + icon: , + title: "Flexible Options", + description: "Multiple ISP configs available, IPv4/PPPoE if needed", + highlight: "Customizable", + }, + ]; + + return ( +
+ {/* Header */} +
+

+ Why Choose Us +

+
+ + {/* Feature grid - 3 columns on large, 2 on medium, 1 on mobile */} +
+ {features.map((feature, index) => ( +
+ +
+ ))} +
+
+ ); +} diff --git a/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx b/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx index e7c33198..1bdea2ae 100644 --- a/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx +++ b/apps/portal/src/features/catalog/views/PublicInternetConfigure.tsx @@ -1,7 +1,7 @@ "use client"; import { useSearchParams } from "next/navigation"; -import { WifiIcon, CheckIcon, ClockIcon, EnvelopeIcon } from "@heroicons/react/24/outline"; +import { WifiIcon, ClockIcon, EnvelopeIcon, CheckCircleIcon } from "@heroicons/react/24/outline"; import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink"; import { InlineAuthSection } from "@/features/auth/components/InlineAuthSection/InlineAuthSection"; import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath"; @@ -12,8 +12,7 @@ import { Skeleton } from "@/components/atoms/loading-skeleton"; /** * Public Internet Configure View * - * Signup flow for internet ordering with honest expectations about - * the verification timeline (1-2 business days, not instant). + * Clean signup flow - auth form is the focus, "what happens next" is secondary info. */ export function PublicInternetConfigureView() { const shopBasePath = useShopBasePath(); @@ -42,147 +41,89 @@ export function PublicInternetConfigureView() { {/* Header */} -
+
-
- +
+
-

- Request Internet Service +

+ Check Internet Service Availability

-

- Create an account to request an availability check for your address. +

+ Create an account to see what's available at your address

{/* Plan Summary Card - only if plan is selected */} {plan && ( -
-
- Selected Plan -
-
-
-
- -
+
+
+
+
-
+
-

{plan.name}

- {plan.description && ( -

{plan.description}

- )} - {(plan.catalogMetadata?.tierDescription || - plan.internetPlanTier || - plan.internetOfferingType) && ( -
- {(plan.catalogMetadata?.tierDescription || plan.internetPlanTier) && ( - - {plan.catalogMetadata?.tierDescription || plan.internetPlanTier} - - )} - {plan.internetOfferingType && ( - - {plan.internetOfferingType} - - )} -
- )} -
-
- +

Selected plan

+

{plan.name}

+
)} - {/* What happens after signup - honest timeline */} -
-

What happens next

-
-
-
- 1 -
-
-

Create your account

-

- Sign up with your service address to start the process. -

-
-
-
-
- 2 -
-
-
-

We verify availability

- - - 1-2 business days - -
-

- Our team checks service availability with NTT for your specific address. -

-
-
-
-
- 3 -
-
-
-

- You receive email notification -

- -
-

- We'll email you when your personalized plans are ready to view. -

-
-
-
-
- 4 -
-
-

Complete your order

-

- Choose your plan options, add payment, and schedule installation. -

-
-
-
-
- - {/* Important note */} -
-
- -
-

Your account is ready immediately

-

- While we verify your address, you can explore your account, add payment methods, and - browse our other services like SIM and VPN. -

-
-
-
- - {/* Auth Section */} + {/* Auth Section - Primary focus */} + + {/* What happens next - Below auth, secondary info */} +
+

What happens next

+
+
+
+ 1 +
+
+

We verify your address

+

+ + 1-2 business days +

+
+
+
+
+ 2 +
+
+

You get notified

+

+ + Email when ready +

+
+
+
+
+ 3 +
+
+

Complete your order

+

+ + Choose plan & schedule +

+
+
+
+
); } diff --git a/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx b/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx index de901a32..f72fc524 100644 --- a/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx +++ b/apps/portal/src/features/catalog/views/PublicInternetPlans.tsx @@ -1,7 +1,12 @@ "use client"; -import { useMemo } from "react"; -import { ServerIcon, ArrowRightIcon } from "@heroicons/react/24/outline"; +import { useMemo, useState } from "react"; +import { + ArrowRightIcon, + SparklesIcon, + ChevronDownIcon, + ChevronUpIcon, +} from "@heroicons/react/24/outline"; import { useInternetCatalog } from "@/features/catalog/hooks"; import type { InternetPlanCatalogItem, @@ -10,365 +15,396 @@ import type { import { Skeleton } from "@/components/atoms/loading-skeleton"; import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink"; -import { CatalogHero } from "@/features/catalog/components/base/CatalogHero"; import { useShopBasePath } from "@/features/catalog/hooks/useShopBasePath"; -import { InternetImportantNotes } from "@/features/catalog/components/internet/InternetImportantNotes"; -import { - InternetOfferingCard, - type TierInfo, -} from "@/features/catalog/components/internet/InternetOfferingCard"; -import { WhyChooseSection } from "@/features/catalog/components/internet/WhyChooseSection"; -import { PlanComparisonGuide } from "@/features/catalog/components/internet/PlanComparisonGuide"; -import { HowItWorksSection } from "@/features/catalog/components/internet/HowItWorksSection"; import { Button } from "@/components/atoms/button"; +// Streamlined components +import { WhyChooseUsPillars } from "@/features/catalog/components/internet/WhyChooseUsPillars"; +import { PublicOfferingCard } from "@/features/catalog/components/internet/PublicOfferingCard"; +import type { TierInfo } from "@/features/catalog/components/internet/PublicOfferingCard"; + // Types -interface OfferingConfig { +interface GroupedOffering { offeringType: string; title: string; speedBadge: string; description: string; iconType: "home" | "apartment"; - isPremium: boolean; - displayOrder: number; + startingPrice: number; + setupFee: number; + tiers: TierInfo[]; + isPremium?: boolean; + showConnectionInfo?: boolean; } -// Display order optimized for UX: -// 1. Apartment 1G - Most common in Tokyo/Japan (many people live in mansions/apartments) -// 2. Apartment 100M - Second most common for apartments (older buildings) -// 3. Home 1G - Most common for houses -// 4. Home 10G - Premium option, select areas only -const OFFERING_CONFIGS: OfferingConfig[] = [ +// FAQ data +const faqItems = [ { - offeringType: "Apartment 1G", - title: "Apartment 1Gbps", - speedBadge: "1 Gbps", - description: "High-speed fiber-to-the-unit for mansions and apartment buildings.", - iconType: "apartment", - isPremium: false, - displayOrder: 1, + question: "How can I check if 10Gbps service is available at my address?", + answer: + "10Gbps service is currently available in select areas, primarily in Tokyo and surrounding regions. When you check availability with your address, we'll show you exactly which speed options are available at your location.", }, { - offeringType: "Apartment 100M", - title: "Apartment 100Mbps", - speedBadge: "100 Mbps", - description: - "Standard speed via VDSL or LAN for apartment buildings with shared infrastructure.", - iconType: "apartment", - isPremium: false, - displayOrder: 2, + question: "Why do apartment speeds vary by building?", + answer: + "Apartment buildings have different NTT fiber infrastructure. Newer buildings often have FTTH (fiber-to-the-home) supporting up to 1Gbps, while older buildings may use VDSL or LAN connections at 100Mbps. The good news: all apartment types have the same monthly price.", }, { - offeringType: "Home 1G", - title: "Home 1Gbps", - speedBadge: "1 Gbps", - description: - "High-speed fiber for standalone houses. The most popular choice for home internet.", - iconType: "home", - isPremium: false, - displayOrder: 3, + question: "My home needs multiple WiFi routers for full coverage. Can you help?", + answer: + "Yes! Our Platinum tier includes a mesh WiFi system designed for larger homes. During setup, our team will assess your space and recommend the best equipment configuration for full coverage.", }, { - offeringType: "Home 10G", - title: "Home 10Gbps", - speedBadge: "10 Gbps", - description: - "Ultra-fast fiber for standalone houses with the highest speeds available in Japan.", - iconType: "home", - isPremium: true, - displayOrder: 4, + question: "Can I transfer my existing internet service to Assist Solutions?", + answer: + "In most cases, yes. If you already have an NTT line, we can often take over the service without a new installation. Contact us with your current provider details and we'll guide you through the process.", + }, + { + question: "What is the contract period?", + answer: + "Our standard contract is 2 years. Early termination fees may apply if you cancel before the contract ends. The setup fee can be paid upfront or spread across 12 or 24 monthly installments.", + }, + { + question: "How are invoices sent?", + answer: + "E-statements (available only in English) will be sent to your primary email address. The service fee will be charged automatically to your registered credit card on file. For corporate plans, please contact us with your requests.", }, ]; /** - * Get tier info from plans + * FAQ Item component with expand/collapse */ -function getTierInfo(plans: InternetPlanCatalogItem[], offeringType: string): TierInfo[] { - const filtered = plans.filter(p => p.internetOfferingType === offeringType); - - const tierOrder: ("Silver" | "Gold" | "Platinum")[] = ["Silver", "Gold", "Platinum"]; - - const tierDescriptions: Record< - string, - { description: string; features: string[]; pricingNote?: string } - > = { - Silver: { - description: "Essential setup—bring your own router", - features: [ - "NTT modem + ISP connection", - "IPoE or PPPoE protocols", - "Self-configuration required", - ], - }, - Gold: { - description: "All-inclusive with router rental", - features: [ - "Everything in Silver, plus:", - "WiFi router included", - "Auto-configured within 24hrs", - "Range extender option (+¥500/mo)", - ], - }, - Platinum: { - description: "Tailored setup for larger residences", - features: [ - "Netgear INSIGHT mesh routers", - "Cloud-managed WiFi network", - "Remote support & auto-updates", - "Custom setup for your space", - ], - pricingNote: "+ equipment fees based on your home", - }, - }; - - const result: TierInfo[] = []; - - for (const tier of tierOrder) { - const plan = filtered.find(p => p.internetPlanTier?.toLowerCase() === tier.toLowerCase()); - - if (!plan) continue; - - const config = tierDescriptions[tier]; - - result.push({ - tier, - monthlyPrice: plan.monthlyPrice ?? 0, - description: config.description, - features: config.features, - recommended: tier === "Gold", - pricingNote: config.pricingNote, - }); - } - - return result; -} - -/** - * Get the setup fee from installations - */ -function getSetupFee(installations: InternetInstallationCatalogItem[]): number { - const basic = installations.find(i => i.sku?.toLowerCase().includes("basic")); - return basic?.oneTimePrice ?? 22800; -} - -/** - * Public Internet Plans View - * - * Displays internet plans for unauthenticated users. - * Uses an informational approach - users can browse plans but must sign up - * and verify their address before they can actually order. - */ -export function PublicInternetPlansView() { - const shopBasePath = useShopBasePath(); - const { data, isLoading, error } = useInternetCatalog(); - const plans: InternetPlanCatalogItem[] = useMemo(() => data?.plans ?? [], [data?.plans]); - const installations: InternetInstallationCatalogItem[] = useMemo( - () => data?.installations ?? [], - [data?.installations] - ); - - const setupFee = useMemo(() => getSetupFee(installations), [installations]); - - // Build offering cards data - const offeringCards = useMemo(() => { - return OFFERING_CONFIGS.map(config => { - const tiers = getTierInfo(plans, config.offeringType); - const startingPrice = tiers.length > 0 ? Math.min(...tiers.map(t => t.monthlyPrice)) : 0; - - return { - ...config, - tiers, - startingPrice, - setupFee, - ctaPath: `/shop/internet/configure`, - }; - }) - .filter(card => card.tiers.length > 0) - .sort((a, b) => a.displayOrder - b.displayOrder); - }, [plans, setupFee]); - - if (isLoading) { - return ( -
- - -
- - -
- -
- {Array.from({ length: 4 }).map((_, i) => ( -
-
- -
- - - -
-
-
- ))} -
-
- ); - } - - if (error) { - return ( -
- - - Please try again later or contact support if the problem persists. - -
- ); - } - +function FAQItem({ + question, + answer, + isOpen, + onToggle, +}: { + question: string; + answer: string; + isOpen: boolean; + onToggle: () => void; +}) { return ( -
- - - - - {offeringCards.length > 0 ? ( - <> - {/* SECTION 1: Why choose us - Build trust first */} -
- -
- - {/* SECTION 2: How it works - Set expectations */} -
- -
- - {/* SECTION 3: Primary CTA - Get started */} -
-
-
-

Ready to get connected?

-

- Create an account and we'll verify what service is available at your address. - You'll receive an email within 1-2 business days. -

-
- -
-
- - {/* SECTION 4: Plan tiers explained - Educational */} -
-
-

Service tiers explained

-

- All connection types offer three service levels. You'll choose your tier after we - verify your address. -

-
- -
- - {/* SECTION 5: Available connection types - Preview only */} -
-
-

Available connection types

-

- Which type applies to you depends on your building. Expand any card to preview - pricing. -

-
- -
- {offeringCards.map(card => ( - - ))} -
- - {/* Note about preview mode */} -

- Pricing shown is for reference. Your actual options will be confirmed after address - verification. -

-
- - {/* SECTION 6: Important notes */} -
- -
- - {/* SECTION 7: Final CTA */} -
-

- Not sure which plan is right for you? -

-

- Don't worry—just sign up and we'll figure it out together. Our team will verify your - address and show you exactly which plans are available. -

- -
- - - - ) : ( -
-
- -

No Plans Available

-

- We couldn't find any internet plans available at this time. -

- -
+
+ + {isOpen && ( +
+

{answer}

)}
); } -export default PublicInternetPlansView; +/** + * Public Internet Plans page - Marketing/Conversion focused + * Clean, polished design optimized for conversion + * + * Note: Apartment types (FTTH 1G, VDSL 100M, LAN 100M) are consolidated into a single + * "Apartment" offering since they all have the same pricing. The actual connection type + * is determined by the building infrastructure during eligibility check. + */ +export function PublicInternetPlansView() { + const { data: catalog, isLoading, error } = useInternetCatalog(); + const shopBasePath = useShopBasePath(); + const ctaPath = `${shopBasePath}/internet/configure`; + const [openFaqIndex, setOpenFaqIndex] = useState(null); + + // Group catalog items by offering type + const groupedOfferings = useMemo(() => { + if (!catalog?.plans) return []; + + const plansByType = catalog.plans.reduce( + (acc, plan) => { + const key = plan.internetOfferingType ?? "unknown"; + if (!acc[key]) acc[key] = []; + acc[key].push(plan); + return acc; + }, + {} as Record + ); + + // Get installation item for setup fee + const installationItem = catalog.installations?.[0] as + | InternetInstallationCatalogItem + | undefined; + const setupFee = installationItem?.oneTimePrice ?? 22800; + + // Create grouped offerings + const offerings: GroupedOffering[] = []; + + // Consolidate apartment types (they all have the same price) + // Connection type (FTTH, VDSL, LAN) depends on building infrastructure + const apartmentTypes = ["Apartment 1G", "Apartment 100M"]; + const apartmentPlans: InternetPlanCatalogItem[] = []; + + for (const type of apartmentTypes) { + if (plansByType[type]) { + apartmentPlans.push(...plansByType[type]); + } + } + + // Define offering metadata + // Order: Home 10G first (premium), then Home 1G, then consolidated Apartment + const offeringMeta: Record< + string, + { + title: string; + description: string; + iconType: "home" | "apartment"; + order: number; + isPremium?: boolean; + } + > = { + "Home 10G": { + title: "Home 10Gbps", + description: "Ultra-fast fiber with the highest speeds available in Japan.", + iconType: "home", + order: 1, + isPremium: true, + }, + "Home 1G": { + title: "Home 1Gbps", + description: "High-speed fiber. The most popular choice for home internet.", + iconType: "home", + order: 2, + }, + Apartment: { + title: "Apartment", + description: + "For mansions and apartment buildings. Speed depends on your building (up to 1Gbps).", + iconType: "apartment", + order: 3, + }, + }; + + // Process Home offerings + for (const [offeringType, plans] of Object.entries(plansByType)) { + // Skip apartment types - we'll handle them separately + if (apartmentTypes.includes(offeringType)) continue; + + const meta = offeringMeta[offeringType]; + if (!meta) continue; + + // Sort plans by tier: Silver, Gold, Platinum + const tierOrder: Record = { Silver: 0, Gold: 1, Platinum: 2 }; + const sortedPlans = [...plans].sort( + (a, b) => + (tierOrder[a.internetPlanTier ?? ""] ?? 99) - (tierOrder[b.internetPlanTier ?? ""] ?? 99) + ); + + // Calculate starting price + const startingPrice = Math.min(...sortedPlans.map(p => p.monthlyPrice ?? 0)); + + // Get speed from offering type + const speedBadge = getSpeedBadge(offeringType); + + // Build tier info (no recommended badge in public view) + const tiers: TierInfo[] = sortedPlans.map(plan => ({ + tier: (plan.internetPlanTier ?? "Silver") as TierInfo["tier"], + monthlyPrice: plan.monthlyPrice ?? 0, + description: getTierDescription(plan.internetPlanTier ?? ""), + features: plan.catalogMetadata?.features ?? getTierFeatures(plan.internetPlanTier ?? ""), + pricingNote: plan.internetPlanTier === "Platinum" ? "+ equipment fees" : undefined, + })); + + offerings.push({ + offeringType, + title: meta.title, + speedBadge, + description: meta.description, + iconType: meta.iconType, + startingPrice, + setupFee, + tiers, + isPremium: meta.isPremium, + }); + } + + // Add consolidated Apartment offering (use any apartment plan for tiers - prices are the same) + if (apartmentPlans.length > 0) { + const meta = offeringMeta["Apartment"]; + + // Get unique tiers from apartment plans (they all have same prices) + const tierOrder: Record = { Silver: 0, Gold: 1, Platinum: 2 }; + const uniqueTiers = new Map(); + + for (const plan of apartmentPlans) { + const tier = plan.internetPlanTier ?? "Silver"; + // Keep first occurrence of each tier (prices are same across apartment types) + if (!uniqueTiers.has(tier)) { + uniqueTiers.set(tier, plan); + } + } + + const sortedTierPlans = Array.from(uniqueTiers.values()).sort( + (a, b) => + (tierOrder[a.internetPlanTier ?? ""] ?? 99) - (tierOrder[b.internetPlanTier ?? ""] ?? 99) + ); + + const startingPrice = Math.min(...sortedTierPlans.map(p => p.monthlyPrice ?? 0)); + + const tiers: TierInfo[] = sortedTierPlans.map(plan => ({ + tier: (plan.internetPlanTier ?? "Silver") as TierInfo["tier"], + monthlyPrice: plan.monthlyPrice ?? 0, + description: getTierDescription(plan.internetPlanTier ?? ""), + features: plan.catalogMetadata?.features ?? getTierFeatures(plan.internetPlanTier ?? ""), + pricingNote: plan.internetPlanTier === "Platinum" ? "+ equipment fees" : undefined, + })); + + offerings.push({ + offeringType: "Apartment", + title: meta.title, + speedBadge: "Up to 1Gbps", + description: meta.description, + iconType: meta.iconType, + startingPrice, + setupFee, + tiers, + showConnectionInfo: true, // Show the info tooltip for Apartment + }); + } + + // Sort by order + return offerings.sort((a, b) => { + const orderA = offeringMeta[a.offeringType]?.order ?? 99; + const orderB = offeringMeta[b.offeringType]?.order ?? 99; + return orderA - orderB; + }); + }, [catalog]); + + // Error state + if (error) { + return ( +
+ + + We couldn't load internet plans. Please try again later. + +
+ ); + } + + return ( +
+ {/* Back link */} + + + {/* Hero - Clean and impactful */} +
+

+ Internet Service Plans +

+

+ NTT Optical Fiber with full English support +

+
+ + {/* Why choose us - 3 pillars */} + + + {/* Connection types - no extra header text */} +
+ {isLoading ? ( +
+ {[1, 2, 3].map(i => ( + + ))} +
+ ) : ( +
+ {groupedOfferings.map((offering, index) => ( + + ))} +
+ )} +
+ + {/* Final CTA - Polished */} +
+
+ + Get started in minutes +
+

Ready to get connected?

+

+ Enter your address to see what's available at your location +

+ +
+ + {/* FAQ Section */} +
+

Frequently Asked Questions

+
+ {faqItems.map((item, index) => ( + setOpenFaqIndex(openFaqIndex === index ? null : index)} + /> + ))} +
+
+
+ ); +} + +// Helper functions +function getSpeedBadge(offeringType: string): string { + const speeds: Record = { + "Apartment 100M": "100Mbps", + "Apartment 1G": "1Gbps", + "Home 1G": "1Gbps", + "Home 10G": "10Gbps", + }; + return speeds[offeringType] ?? "1Gbps"; +} + +function getTierDescription(tier: string): string { + const descriptions: Record = { + Silver: "Use your own router. Best for tech-savvy users.", + Gold: "Includes WiFi router rental. Our most popular choice.", + Platinum: "Premium equipment with mesh WiFi for larger homes.", + }; + return descriptions[tier] ?? ""; +} + +function getTierFeatures(tier: string): string[] { + const features: Record = { + Silver: ["NTT modem + ISP connection", "Use your own router", "Email/ticket support"], + Gold: ["NTT modem + ISP connection", "WiFi router included", "Priority phone support"], + Platinum: ["NTT modem + ISP connection", "Mesh WiFi system included", "Dedicated support line"], + }; + return features[tier] ?? []; +}