Refactor internet and SIM plan components for improved UI and functionality

- Removed unused imports and components for cleaner code.
- Updated styling and layout in InternetPlanCard for better visual consistency.
- Introduced CatalogBackLink for navigation in InternetConfigureContainer and SimConfigureView.
- Enhanced loading states and error handling in InternetPlans and SimPlans views.
- Improved button interactions and accessibility in ActivationForm and SimConfigureView.
- Streamlined plan display logic and added dynamic features in various components.
This commit is contained in:
barsa 2025-10-22 14:36:51 +09:00
parent 3d17f36c2f
commit da91a51323
9 changed files with 341 additions and 306 deletions

View File

@ -0,0 +1,49 @@
"use client";
import { Button } from "@/components/atoms/button";
import { cn } from "@/lib/utils";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import type { ReactNode } from "react";
type Alignment = "left" | "center" | "right";
interface CatalogBackLinkProps {
href: string;
label?: string;
align?: Alignment;
className?: string;
buttonClassName?: string;
icon?: ReactNode;
}
const alignmentMap: Record<Alignment, string> = {
left: "justify-start",
center: "justify-center",
right: "justify-end",
};
export function CatalogBackLink({
href,
label = "Back",
align = "left",
className,
buttonClassName,
icon = <ArrowLeftIcon className="w-4 h-4" />,
}: CatalogBackLinkProps) {
return (
<div className={cn("mb-6 flex", alignmentMap[align], className)}>
<Button
as="a"
href={href}
size="sm"
variant="ghost"
leftIcon={icon}
className={cn("text-gray-600 hover:text-gray-900", buttonClassName)}
>
{label}
</Button>
</div>
);
}
export type { CatalogBackLinkProps };

View File

@ -0,0 +1,47 @@
"use client";
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
type Alignment = "left" | "center";
interface CatalogHeroProps {
title: string;
description: string;
align?: Alignment;
eyebrow?: ReactNode;
children?: ReactNode;
className?: string;
}
const alignmentMap: Record<Alignment, string> = {
left: "text-left items-start",
center: "text-center items-center",
};
export function CatalogHero({
title,
description,
align = "center",
eyebrow,
children,
className,
}: CatalogHeroProps) {
return (
<div
className={cn(
"flex flex-col gap-4 mb-12",
alignmentMap[align],
className,
align === "center" ? "mx-auto max-w-3xl" : ""
)}
>
{eyebrow ? <div className="text-sm font-medium text-blue-700">{eyebrow}</div> : null}
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 leading-tight">{title}</h1>
<p className="text-lg text-gray-600 leading-relaxed">{description}</p>
{children ? <div className="mt-2 w-full">{children}</div> : null}
</div>
);
}
export type { CatalogHeroProps };

View File

@ -2,7 +2,7 @@
import { AnimatedCard } from "@/components/molecules";
import { Button } from "@/components/atoms/button";
import { CurrencyYenIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import type {
InternetPlanCatalogItem,
InternetInstallationCatalogItem,
@ -27,6 +27,8 @@ export function InternetPlanCard({
const isGold = tier === "Gold";
const isPlatinum = tier === "Platinum";
const isSilver = tier === "Silver";
const isDevEnvironment = process.env.NODE_ENV === "development";
const isDisabled = disabled && !isDevEnvironment;
const installationPrices = installations
.map(installation => {
@ -45,59 +47,61 @@ export function InternetPlanCard({
const getBorderClass = () => {
if (isGold)
return "border-2 border-yellow-400/50 bg-gradient-to-br from-yellow-50/80 to-amber-50/80 backdrop-blur-sm shadow-xl hover:shadow-2xl ring-2 ring-yellow-200/30";
return "border border-yellow-200 bg-white shadow-lg hover:shadow-xl ring-1 ring-yellow-100";
if (isPlatinum)
return "border-2 border-indigo-400/50 bg-gradient-to-br from-indigo-50/80 to-purple-50/80 backdrop-blur-sm shadow-xl hover:shadow-2xl ring-2 ring-indigo-200/30";
if (isSilver)
return "border-2 border-gray-300/50 bg-gradient-to-br from-gray-50/80 to-slate-50/80 backdrop-blur-sm shadow-xl hover:shadow-2xl ring-2 ring-gray-200/30";
return "border border-gray-200/50 bg-white/80 backdrop-blur-sm shadow-lg hover:shadow-xl";
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";
return "border border-gray-200 bg-white shadow hover:shadow-lg";
};
return (
<AnimatedCard
variant="static"
className={`overflow-hidden flex flex-col h-full transition-all duration-500 ease-out hover:-translate-y-2 hover:scale-[1.02] ${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">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<span
className={`px-3 py-1 rounded-full text-sm font-medium border ${
isGold
? "bg-yellow-100 text-yellow-800 border-yellow-300"
: isPlatinum
? "bg-purple-100 text-purple-800 border-purple-300"
: "bg-gray-100 text-gray-800 border-gray-300"
}`}
>
{tier || "Plan"}
</span>
{isGold && (
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full">
Recommended
<div className="p-6 flex flex-col flex-grow space-y-5">
<div className="flex items-start justify-between gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span
className={`px-2.5 py-1 rounded-full text-xs font-medium border ${
isGold
? "bg-yellow-50 text-yellow-700 border-yellow-200"
: isPlatinum
? "bg-indigo-50 text-indigo-700 border-indigo-200"
: "bg-gray-50 text-gray-700 border-gray-200"
}`}
>
{tier || "Plan"}
</span>
)}
</div>
{plan.monthlyPrice && plan.monthlyPrice > 0 && (
<div className="text-right">
<div className="flex items-baseline justify-end gap-1 text-2xl font-bold text-gray-900">
<CurrencyYenIcon className="h-6 w-6" />
<span>{plan.monthlyPrice.toLocaleString()}</span>
<span className="text-sm text-gray-500 font-normal whitespace-nowrap">
per month
{isGold && (
<span className="px-2 py-0.5 text-xs font-medium text-green-700 bg-green-50 border border-green-200 rounded-full">
Recommended
</span>
)}
</div>
<h3 className="text-lg font-semibold text-gray-900 leading-snug max-w-xs">{plan.name}</h3>
</div>
{plan.monthlyPrice && plan.monthlyPrice > 0 && (
<div className="text-right shrink-0">
<div className="text-xs uppercase tracking-wide text-gray-500 mb-1">Monthly</div>
<div className="text-2xl font-bold text-gray-900 leading-none">
¥{plan.monthlyPrice.toLocaleString()}
</div>
{plan.oneTimePrice && plan.oneTimePrice > 0 && (
<div className="text-xs text-gray-500 mt-1">One-time ¥{plan.oneTimePrice.toLocaleString()}</div>
)}
</div>
)}
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">{plan.name}</h3>
<p className="text-gray-600 text-sm mb-4">
<p className="text-gray-600 text-sm leading-relaxed">
{plan.catalogMetadata?.tierDescription || plan.description}
</p>
<div className="mb-6 flex-grow">
<h4 className="font-medium text-gray-900 mb-3">Your Plan Includes:</h4>
<div className="flex-grow">
<h4 className="font-medium text-gray-900 mb-3">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) => (
@ -135,14 +139,14 @@ export function InternetPlanCard({
<Button
className="w-full group"
disabled={disabled}
rightIcon={!disabled ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
disabled={isDisabled}
rightIcon={!isDisabled ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
onClick={() => {
if (disabled) return;
if (isDisabled) return;
router.push(`/catalog/internet/configure?plan=${plan.sku}`);
}}
>
{disabled ? disabledReason || "Not available" : "Configure Plan"}
{isDisabled ? disabledReason || "Not available" : "Configure Plan"}
</Button>
</div>
</AnimatedCard>

View File

@ -2,8 +2,8 @@
import { PageLayout } from "@/components/templates/PageLayout";
import { ProgressSteps } from "@/components/molecules";
import { Button } from "@/components/atoms/button";
import { ServerIcon, ArrowLeftIcon } from "@heroicons/react/24/outline";
import { ServerIcon } from "@heroicons/react/24/outline";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import type {
InternetPlanCatalogItem,
InternetInstallationCatalogItem,
@ -85,18 +85,7 @@ export function InternetConfigureContainer({
description="Set up your internet service options"
>
<div className="max-w-4xl mx-auto">
<div className="mb-6">
<Button
as="a"
href="/catalog/internet"
variant="ghost"
size="sm"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
className="text-gray-600 hover:text-gray-900"
>
Back to Internet Plans
</Button>
</div>
<CatalogBackLink href="/catalog/internet" label="Back to Internet Plans" />
{/* Plan Header */}
<PlanHeader plan={plan} monthlyTotal={monthlyTotal} oneTimeTotal={oneTimeTotal} />

View File

@ -13,12 +13,15 @@ export function ActivationForm({
onScheduledActivationDateChange,
errors,
}: ActivationFormProps) {
const sharedLabelClasses =
"flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors duration-200 ease-in-out";
return (
<div className="space-y-4">
<label
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-all duration-300 ease-in-out hover:scale-[1.01] ${
className={`${sharedLabelClasses} ${
activationType === "Immediate"
? "border-blue-500 bg-blue-50 ring-2 ring-blue-100 shadow-sm"
? "border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-200"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
}`}
>
@ -39,9 +42,9 @@ export function ActivationForm({
</label>
<label
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-all duration-300 ease-in-out hover:scale-[1.01] ${
className={`${sharedLabelClasses} ${
activationType === "Scheduled"
? "border-blue-500 bg-blue-50 ring-2 ring-blue-100 shadow-sm"
? "border-blue-500 bg-blue-50 shadow-sm ring-1 ring-blue-200"
: "border-gray-200 hover:border-blue-300 hover:bg-blue-50"
}`}
>
@ -59,7 +62,12 @@ export function ActivationForm({
Choose a specific date for activation (up to 30 days from today)
</p>
{activationType === "Scheduled" && (
<div
className={`overflow-hidden transition-[max-height,opacity] duration-300 ease-out ${
activationType === "Scheduled" ? "max-h-[240px] opacity-100" : "max-h-0 opacity-0"
}`}
aria-hidden={activationType !== "Scheduled"}
>
<div className="mt-3">
<label
htmlFor="scheduledActivationDate"
@ -84,7 +92,7 @@ export function ActivationForm({
requests may be processed on the next business day.
</p>
</div>
)}
</div>
</div>
</label>
</div>

View File

@ -9,6 +9,7 @@ import { SimTypeSelector } from "@/features/catalog/components/sim/SimTypeSelect
import { ActivationForm } from "@/features/catalog/components/sim/ActivationForm";
import { MnpForm } from "@/features/catalog/components/sim/MnpForm";
import { ProgressSteps } from "@/components/molecules";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import {
ArrowLeftIcon,
ArrowRightIcon,
@ -138,18 +139,7 @@ export function SimConfigureView({
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-4xl mx-auto space-y-8">
<div className="mb-6">
<Button
as="a"
href="/catalog/sim"
variant="ghost"
size="sm"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
className="text-gray-600 hover:text-gray-900"
>
Back to SIM Plans
</Button>
</div>
<CatalogBackLink href="/catalog/sim" label="Back to SIM Plans" />
<AnimatedCard variant="static" className="p-6">
<div className="flex justify-between items-start">
@ -228,7 +218,12 @@ export function SimConfigureView({
/>
<div className="flex justify-end mt-6">
<Button
onClick={() => transitionToStep(2)}
onClick={() => {
if (simType === "eSIM" && !validate()) {
return;
}
transitionToStep(2);
}}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue to Activation
@ -265,7 +260,12 @@ export function SimConfigureView({
Back to SIM Type
</Button>
<Button
onClick={() => transitionToStep(3)}
onClick={() => {
if (activationType === "Scheduled" && !validate()) {
return;
}
transitionToStep(3);
}}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}
>
Continue to Add-ons
@ -349,7 +349,7 @@ export function SimConfigureView({
</Button>
<Button
onClick={() => {
if (wantsMnp && !validate()) return;
if ((wantsMnp || activationType === "Scheduled") && !validate()) return;
transitionToStep(5);
}}
rightIcon={<ArrowRightIcon className="w-4 h-4" />}

View File

@ -2,13 +2,7 @@
import { useState, useEffect, useMemo } from "react";
import { PageLayout } from "@/components/templates/PageLayout";
import {
WifiIcon,
ServerIcon,
ArrowLeftIcon,
HomeIcon,
BuildingOfficeIcon,
} from "@heroicons/react/24/outline";
import { WifiIcon, ServerIcon, HomeIcon, BuildingOfficeIcon } from "@heroicons/react/24/outline";
import { useInternetCatalog } from "@/features/catalog/hooks";
import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscriptions";
import type {
@ -17,9 +11,10 @@ import type {
} from "@customer-portal/domain/catalog";
import { Skeleton } from "@/components/atoms/loading-skeleton";
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
import { Button } from "@/components/atoms/button";
import { InternetPlanCard } from "@/features/catalog/components/internet/InternetPlanCard";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { CatalogHero } from "@/features/catalog/components/base/CatalogHero";
export function InternetPlansContainer() {
const { data, isLoading, error } = useInternetCatalog();
@ -62,101 +57,77 @@ export function InternetPlansContainer() {
if (isLoading || error) {
return (
<PageLayout
title="Internet Plans"
description="Loading your personalized plans..."
icon={<WifiIcon className="h-6 w-6" />}
>
<AsyncBlock isLoading={false} error={error}>
<div className="max-w-6xl mx-auto px-4">
{/* Back */}
<div className="mb-6">
<div className="h-9 w-44 bg-gray-200 rounded" />
</div>
<div className="min-h-screen bg-slate-50">
<PageLayout
title="Internet Plans"
description="Loading your personalized plans..."
icon={<WifiIcon className="h-6 w-6" />}
>
<AsyncBlock isLoading={false} error={error}>
<div className="max-w-6xl mx-auto px-4">
<CatalogBackLink href="/catalog" label="Back to Services" />
{/* Title + eligibility */}
<div className="text-center mb-12">
<div className="h-10 w-96 bg-gray-200 rounded mx-auto mb-4" />
<div className="mt-6 inline-flex items-center gap-2 px-6 py-3 rounded-2xl border">
<div className="h-5 w-5 bg-gray-200 rounded" />
<div className="h-4 w-56 bg-gray-200 rounded" />
</div>
<div className="h-4 w-[32rem] max-w-full bg-gray-200 rounded mx-auto mt-2" />
</div>
{/* Active internet warning slot */}
<div className="mb-8 h-20 bg-yellow-50 border border-yellow-200 rounded-xl" />
{/* Plans grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="bg-white rounded-xl border border-gray-200 p-6 space-y-3">
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-10 w-full" />
{/* Title + eligibility */}
<div className="text-center mb-12">
<div className="h-10 w-96 bg-gray-200 rounded mx-auto mb-4" />
<div className="mt-6 inline-flex items-center gap-2 px-6 py-3 rounded-2xl border">
<div className="h-5 w-5 bg-gray-200 rounded" />
<div className="h-4 w-56 bg-gray-200 rounded" />
</div>
))}
</div>
<div className="h-4 w-[32rem] max-w-full bg-gray-200 rounded mx-auto mt-2" />
</div>
{/* Important Notes */}
<div className="mt-12 h-24 bg-blue-50 border border-blue-200 rounded-xl" />
</div>
</AsyncBlock>
</PageLayout>
{/* Active internet warning slot */}
<div className="mb-8 h-20 bg-yellow-50 border border-yellow-200 rounded-xl" />
{/* Plans grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="bg-white rounded-xl border border-gray-200 p-6 space-y-3">
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-10 w-full" />
</div>
))}
</div>
{/* Important Notes */}
<div className="mt-12 h-24 bg-blue-50 border border-blue-200 rounded-xl" />
</div>
</AsyncBlock>
</PageLayout>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50">
<div className="min-h-screen bg-slate-50">
<PageLayout
title="Internet Plans"
description="High-speed internet services for your home or business"
icon={<WifiIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto">
{/* Enhanced Back Button */}
<div className="mb-8">
<Button
as="a"
href="/catalog"
variant="outline"
size="sm"
className="group bg-white/80 backdrop-blur-sm border-white/50 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-0.5"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
Back to Services
</Button>
</div>
{/* Enhanced Header */}
<div className="text-center mb-16 relative">
{/* Background decoration */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -right-20 w-40 h-40 bg-gradient-to-br from-blue-400/10 to-indigo-600/10 rounded-full blur-3xl"></div>
<div className="absolute -bottom-20 -left-20 w-40 h-40 bg-gradient-to-tr from-indigo-400/10 to-purple-600/10 rounded-full blur-3xl"></div>
</div>
<h1 className="text-5xl font-bold bg-gradient-to-r from-gray-900 via-blue-900 to-indigo-900 bg-clip-text text-transparent mb-6 relative">
Choose Your Internet Plan
</h1>
<p className="text-xl text-gray-600 mb-8 max-w-3xl mx-auto leading-relaxed">
High-speed fiber internet with reliable connectivity for your home or business
</p>
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href="/catalog" label="Back to Services" />
<CatalogHero
title="Choose Your Internet Plan"
description="High-speed fiber internet with reliable connectivity for your home or business."
>
{eligibility && (
<div className="mt-8">
<div className="flex flex-col items-center gap-3">
<div
className={`inline-flex items-center gap-3 px-8 py-4 rounded-2xl border backdrop-blur-sm shadow-lg hover:shadow-xl transition-all duration-300 ${getEligibilityColor(eligibility)}`}
className={`inline-flex items-center gap-3 px-6 py-3 rounded-full border ${getEligibilityColor(eligibility)}`}
>
{getEligibilityIcon(eligibility)}
<span className="font-semibold text-lg">Available for: {eligibility}</span>
<span className="font-semibold">Available for: {eligibility}</span>
</div>
<p className="text-gray-600 mt-4 max-w-2xl mx-auto leading-relaxed">
Plans shown are tailored to your house type and local infrastructure
<p className="text-sm text-gray-600 text-center">
Plans shown are tailored to your house type and local infrastructure.
</p>
</div>
)}
</div>
</CatalogHero>
{hasActiveInternet && (
<AlertBanner
@ -214,9 +185,12 @@ export function InternetPlansContainer() {
<p className="text-gray-600 mb-6">
We couldn&apos;t find any internet plans available for your location at this time.
</p>
<Button as="a" href="/catalog" leftIcon={<ArrowLeftIcon className="w-4 h-4" />}>
Back to Services
</Button>
<CatalogBackLink
href="/catalog"
label="Back to Services"
align="center"
className="mt-4 mb-0"
/>
</div>
)}
</div>

View File

@ -15,6 +15,8 @@ import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { useSimCatalog } from "@/features/catalog/hooks";
import type { SimCatalogProduct } from "@customer-portal/domain/catalog";
import { SimPlanTypeSection } from "@/features/catalog/components/sim/SimPlanTypeSection";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { CatalogHero } from "@/features/catalog/components/base/CatalogHero";
interface PlansByType {
DataOnly: SimCatalogProduct[];
@ -36,63 +38,62 @@ export function SimPlansContainer() {
if (isLoading) {
return (
<PageLayout
title="SIM Plans"
description="Loading plans..."
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto px-4">
{/* Back button area */}
<div className="mb-6 flex justify-center">
<div className="h-9 w-44 bg-gray-200 rounded" />
</div>
<div className="min-h-screen bg-slate-50">
<PageLayout
title="SIM Plans"
description="Loading plans..."
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto px-4">
<CatalogBackLink href="/catalog" label="Back to Services" />
{/* Title block */}
<div className="text-center mb-12">
<div className="h-10 w-80 bg-gray-200 rounded mx-auto mb-4" />
<div className="h-6 w-[36rem] max-w-full bg-gray-200 rounded mx-auto" />
</div>
{/* Title block */}
<div className="text-center mb-12">
<div className="h-10 w-80 bg-gray-200 rounded mx-auto mb-4" />
<div className="h-6 w-[36rem] max-w-full bg-gray-200 rounded mx-auto" />
</div>
{/* Family discount banner slot */}
<div className="mb-8">
<div className="h-20 w-full bg-green-50 border border-green-200 rounded-xl" />
</div>
{/* Family discount banner slot */}
<div className="mb-8">
<div className="h-20 w-full bg-green-50 border border-green-200 rounded-xl" />
</div>
{/* Tabs */}
<div className="mb-8 flex justify-center">
<div className="h-10 w-[32rem] max-w-full bg-gray-200 rounded" />
</div>
{/* Tabs */}
<div className="mb-8 flex justify-center">
<div className="h-10 w-[32rem] max-w-full bg-gray-200 rounded" />
</div>
{/* Plans grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="bg-white rounded-xl border border-gray-200 p-6 space-y-3">
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-10 w-full" />
</div>
))}
</div>
{/* Terms section */}
<div className="mt-8 bg-gray-50 rounded-2xl p-8">
{/* Plans grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex items-start gap-3">
<div className="h-5 w-5 bg-gray-200 rounded" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-3 w-56" />
</div>
<div key={i} className="bg-white rounded-xl border border-gray-200 p-6 space-y-3">
<Skeleton className="h-6 w-1/2" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-10 w-full" />
</div>
))}
</div>
</div>
{/* Important terms banner */}
<div className="mt-8 h-28 bg-yellow-50 border border-yellow-200 rounded-xl" />
</div>
</PageLayout>
{/* Terms section */}
<div className="mt-8 bg-gray-50 rounded-2xl p-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex items-start gap-3">
<div className="h-5 w-5 bg-gray-200 rounded" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-3 w-56" />
</div>
</div>
))}
</div>
</div>
{/* Important terms banner */}
<div className="mt-8 h-28 bg-yellow-50 border border-yellow-200 rounded-xl" />
</div>
</PageLayout>
</div>
);
}
@ -132,42 +133,19 @@ export function SimPlansContainer() {
);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-emerald-50 to-teal-50">
<div className="min-h-screen bg-slate-50">
<PageLayout
title="SIM Plans"
description="Choose your mobile plan with flexible options"
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto px-4">
{/* Enhanced Back Button */}
<div className="mb-8 flex justify-center">
<Button
as="a"
href="/catalog"
variant="outline"
size="sm"
className="group bg-white/80 backdrop-blur-sm border-white/50 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-0.5"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
Back to Services
</Button>
</div>
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href="/catalog" label="Back to Services" />
{/* Enhanced Header */}
<div className="text-center mb-16 relative">
{/* Background decoration */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -right-20 w-40 h-40 bg-gradient-to-br from-emerald-400/10 to-teal-600/10 rounded-full blur-3xl"></div>
<div className="absolute -bottom-20 -left-20 w-40 h-40 bg-gradient-to-tr from-teal-400/10 to-cyan-600/10 rounded-full blur-3xl"></div>
</div>
<h1 className="text-5xl font-bold bg-gradient-to-r from-gray-900 via-emerald-900 to-teal-900 bg-clip-text text-transparent mb-6 relative">
Choose Your SIM Plan
</h1>
<p className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed">
Wide range of data options and voice plans with both physical SIM and eSIM options.
</p>
</div>
<CatalogHero
title="Choose Your SIM Plan"
description="Flexible mobile plans with physical SIM and eSIM options for any device."
/>
{hasExistingSim && (
<AlertBanner variant="success" title="Family Discount Applied" className="mb-8">
@ -187,18 +165,24 @@ export function SimPlansContainer() {
<div className="mb-8 flex justify-center">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
<nav className="-mb-px flex space-x-6" aria-label="Tabs">
<button
onClick={() => setActiveTab("data-voice")}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-300 ease-in-out transform hover:scale-105 ${activeTab === "data-voice" ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"}`}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors duration-200 ${
activeTab === "data-voice"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<PhoneIcon
className={`h-5 w-5 transition-transform duration-300 ${activeTab === "data-voice" ? "scale-110" : ""}`}
/>
<PhoneIcon className="h-5 w-5" />
Data + SMS + Voice
{plansByType.DataSmsVoice.length > 0 && (
<span
className={`bg-blue-100 text-blue-600 text-xs px-2 py-0.5 rounded-full transition-all duration-300 ${activeTab === "data-voice" ? "scale-110 bg-blue-200" : ""}`}
className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${
activeTab === "data-voice"
? "border-blue-100 bg-blue-50 text-blue-600"
: "border-gray-200 text-gray-500"
}`}
>
{plansByType.DataSmsVoice.length}
</span>
@ -206,15 +190,21 @@ export function SimPlansContainer() {
</button>
<button
onClick={() => setActiveTab("data-only")}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-300 ease-in-out transform hover:scale-105 ${activeTab === "data-only" ? "border-purple-500 text-purple-600" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"}`}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors duration-200 ${
activeTab === "data-only"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<GlobeAltIcon
className={`h-5 w-5 transition-transform duration-300 ${activeTab === "data-only" ? "scale-110" : ""}`}
/>
<GlobeAltIcon className="h-5 w-5" />
Data Only
{plansByType.DataOnly.length > 0 && (
<span
className={`bg-purple-100 text-purple-600 text-xs px-2 py-0.5 rounded-full transition-all duration-300 ${activeTab === "data-only" ? "scale-110 bg-purple-200" : ""}`}
className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${
activeTab === "data-only"
? "border-blue-100 bg-blue-50 text-blue-600"
: "border-gray-200 text-gray-500"
}`}
>
{plansByType.DataOnly.length}
</span>
@ -222,15 +212,21 @@ export function SimPlansContainer() {
</button>
<button
onClick={() => setActiveTab("voice-only")}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-300 ease-in-out transform hover:scale-105 ${activeTab === "voice-only" ? "border-orange-500 text-orange-600" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"}`}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors duration-200 ${
activeTab === "voice-only"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<PhoneIcon
className={`h-5 w-5 transition-transform duration-300 ${activeTab === "voice-only" ? "scale-110" : ""}`}
/>
Voice + SMS Only
<CheckIcon className="h-5 w-5" />
Voice Only
{plansByType.VoiceOnly.length > 0 && (
<span
className={`bg-orange-100 text-orange-600 text-xs px-2 py-0.5 rounded-full transition-all duration-300 ${activeTab === "voice-only" ? "scale-110 bg-orange-200" : ""}`}
className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${
activeTab === "voice-only"
? "border-blue-100 bg-blue-50 text-blue-600"
: "border-gray-200 text-gray-500"
}`}
>
{plansByType.VoiceOnly.length}
</span>

View File

@ -1,13 +1,14 @@
"use client";
import { PageLayout } from "@/components/templates/PageLayout";
import { ShieldCheckIcon, ArrowLeftIcon } from "@heroicons/react/24/outline";
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
import { useVpnCatalog } from "@/features/catalog/hooks";
import { LoadingCard } from "@/components/atoms";
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
import { Button } from "@/components/atoms/button";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { VpnPlanCard } from "@/features/catalog/components/vpn/VpnPlanCard";
import { CatalogBackLink } from "@/features/catalog/components/base/CatalogBackLink";
import { CatalogHero } from "@/features/catalog/components/base/CatalogHero";
export function VpnPlansView() {
const { data, isLoading, error } = useVpnCatalog();
@ -16,26 +17,14 @@ export function VpnPlansView() {
if (isLoading || error) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50 to-indigo-50">
<div className="min-h-screen bg-slate-50">
<PageLayout
title="VPN Plans"
description="Loading plans..."
icon={<ShieldCheckIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto">
{/* Enhanced Back Button */}
<div className="mb-8">
<Button
as="a"
href="/catalog"
variant="outline"
size="sm"
className="group bg-white/80 backdrop-blur-sm border-white/50 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-0.5"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
Back to Services
</Button>
</div>
<div className="max-w-6xl mx-auto px-4">
<CatalogBackLink href="/catalog" label="Back to Services" />
<AsyncBlock
isLoading={isLoading}
@ -56,43 +45,19 @@ export function VpnPlansView() {
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50 to-indigo-50">
<div className="min-h-screen bg-slate-50">
<PageLayout
title="VPN Router Rental"
description="Secure VPN router rental"
icon={<ShieldCheckIcon className="h-6 w-6" />}
>
<div className="max-w-6xl mx-auto">
{/* Enhanced Back Button */}
<div className="mb-8">
<Button
as="a"
href="/catalog"
variant="outline"
size="sm"
className="group bg-white/80 backdrop-blur-sm border-white/50 shadow-lg hover:shadow-xl transition-all duration-300 hover:-translate-y-0.5"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
>
Back to Services
</Button>
</div>
<div className="max-w-6xl mx-auto px-4 pb-16">
<CatalogBackLink href="/catalog" label="Back to Services" />
{/* Enhanced Header */}
<div className="text-center mb-16 relative">
{/* Background decoration */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-20 -right-20 w-40 h-40 bg-gradient-to-br from-purple-400/10 to-indigo-600/10 rounded-full blur-3xl"></div>
<div className="absolute -bottom-20 -left-20 w-40 h-40 bg-gradient-to-tr from-indigo-400/10 to-violet-600/10 rounded-full blur-3xl"></div>
</div>
<h1 className="text-5xl font-bold bg-gradient-to-r from-gray-900 via-purple-900 to-indigo-900 bg-clip-text text-transparent mb-6 relative">
SonixNet VPN Router Service
</h1>
<p className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed">
Fast and secure VPN connection to San Francisco or London for accessing geo-restricted
content.
</p>
</div>
<CatalogHero
title="SonixNet VPN Router Service"
description="Fast and secure VPN connections to San Francisco or London using a pre-configured router."
/>
{vpnPlans.length > 0 ? (
<div className="mb-8">
@ -123,9 +88,12 @@ export function VpnPlansView() {
<p className="text-gray-600 mb-6">
We couldn&apos;t find any VPN plans available at this time.
</p>
<Button as="a" href="/catalog" leftIcon={<ArrowLeftIcon className="w-4 h-4" />}>
Back to Services
</Button>
<CatalogBackLink
href="/catalog"
label="Back to Services"
align="center"
className="mt-4 mb-0"
/>
</div>
)}