- Added FreebitTestTrackerService to FreebitClientService for improved API call tracking. - Updated Freebit module to include the new test tracker service. - Refactored HowItWorksSection and PublicInternetPlans to enhance user guidance with clearer steps and improved styling. - Introduced a new VpnHowItWorksSection to provide users with a simplified setup process for VPN services. - Enhanced PublicOfferingCard and VpnPlanCard components with updated styles and additional feature highlights for better user engagement.
332 lines
12 KiB
TypeScript
332 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
ShieldCheck,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
Router,
|
|
Globe,
|
|
Tv,
|
|
Wifi,
|
|
Package,
|
|
Headphones,
|
|
CreditCard,
|
|
Play,
|
|
} from "lucide-react";
|
|
import { usePublicVpnCatalog } from "@/features/services/hooks";
|
|
import { LoadingCard } from "@/components/atoms";
|
|
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
|
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
|
import { VpnPlanCard } from "@/features/services/components/vpn/VpnPlanCard";
|
|
import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink";
|
|
import { ServicesHero } from "@/features/services/components/base/ServicesHero";
|
|
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
|
import {
|
|
ServiceHighlights,
|
|
type HighlightFeature,
|
|
} from "@/features/services/components/base/ServiceHighlights";
|
|
|
|
/**
|
|
* Public VPN Plans View
|
|
*
|
|
* Displays VPN plans for unauthenticated users.
|
|
*/
|
|
export function PublicVpnPlansView() {
|
|
const servicesBasePath = useServicesBasePath();
|
|
const { data, error } = usePublicVpnCatalog();
|
|
const vpnPlans = data?.plans || [];
|
|
const activationFees = data?.activationFees || [];
|
|
// Simple loading check: show skeleton until we have data or an error
|
|
const isLoading = !data && !error;
|
|
|
|
if (isLoading || error) {
|
|
return (
|
|
<div className="max-w-6xl mx-auto px-4">
|
|
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
|
|
|
|
<AsyncBlock
|
|
isLoading={isLoading}
|
|
error={error}
|
|
loadingText="Loading VPN plans..."
|
|
variant="page"
|
|
>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
|
{Array.from({ length: 4 }).map((_, index) => (
|
|
<LoadingCard key={index} className="h-64" />
|
|
))}
|
|
</div>
|
|
</AsyncBlock>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const vpnFeatures: HighlightFeature[] = [
|
|
{
|
|
icon: <Router className="h-6 w-6" />,
|
|
title: "Pre-configured Router",
|
|
description: "Ready to use out of the box — just plug in and connect",
|
|
highlight: "Plug & play",
|
|
},
|
|
{
|
|
icon: <Globe className="h-6 w-6" />,
|
|
title: "US & UK Servers",
|
|
description: "Access content from San Francisco or London regions",
|
|
highlight: "2 locations",
|
|
},
|
|
{
|
|
icon: <Tv className="h-6 w-6" />,
|
|
title: "Streaming Ready",
|
|
description: "Works with Apple TV, Roku, Amazon Fire, and more",
|
|
highlight: "All devices",
|
|
},
|
|
{
|
|
icon: <Wifi className="h-6 w-6" />,
|
|
title: "Separate Network",
|
|
description: "VPN runs on dedicated WiFi, keep regular internet normal",
|
|
highlight: "No interference",
|
|
},
|
|
{
|
|
icon: <Package className="h-6 w-6" />,
|
|
title: "Router Rental Included",
|
|
description: "No equipment purchase — router rental is part of the plan",
|
|
highlight: "No hidden costs",
|
|
},
|
|
{
|
|
icon: <Headphones className="h-6 w-6" />,
|
|
title: "English Support",
|
|
description: "Full English assistance for setup and troubleshooting",
|
|
highlight: "Dedicated help",
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto px-4 pb-16">
|
|
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
|
|
|
|
<ServicesHero
|
|
title="VPN Router Service"
|
|
description="Secure VPN connections to San Francisco or London using a pre-configured router."
|
|
/>
|
|
|
|
{/* Service Highlights */}
|
|
<ServiceHighlights features={vpnFeatures} className="mb-12" />
|
|
|
|
{vpnPlans.length > 0 ? (
|
|
<div className="mb-8">
|
|
<div className="text-center mb-8">
|
|
<span className="text-sm font-semibold text-primary uppercase tracking-wider">
|
|
Choose Your Region
|
|
</span>
|
|
<h2 className="text-2xl font-bold text-foreground mt-1">Available Plans</h2>
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
Select one region per router rental
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
|
{vpnPlans.map(plan => (
|
|
<VpnPlanCard key={plan.id} plan={plan} />
|
|
))}
|
|
</div>
|
|
|
|
{activationFees.length > 0 && (
|
|
<AlertBanner variant="info" className="mt-6 max-w-4xl mx-auto" title="Activation Fee">
|
|
A one-time activation fee of ¥3,000 applies per router rental. Tax (10%) not included.
|
|
</AlertBanner>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<ShieldCheck className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-foreground mb-2">No VPN Plans Available</h3>
|
|
<p className="text-muted-foreground mb-6">
|
|
We couldn't find any VPN plans available at this time.
|
|
</p>
|
|
<ServicesBackLink
|
|
href={servicesBasePath}
|
|
label="Back to Services"
|
|
align="center"
|
|
className="mt-4 mb-0"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* How It Works Section */}
|
|
<VpnHowItWorksSection />
|
|
|
|
{/* FAQ Section */}
|
|
<VpnFaqSection />
|
|
|
|
<AlertBanner variant="warning" title="Important Disclaimer" className="mb-8">
|
|
<p className="text-sm">
|
|
Content subscriptions are NOT included in the VPN package. Our VPN service establishes a
|
|
network connection that virtually locates you in the designated server location. Not all
|
|
services can be unblocked. We do not guarantee access to any specific website or streaming
|
|
service quality.
|
|
</p>
|
|
</AlertBanner>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// VPN FAQ Data
|
|
const vpnFaqItems = [
|
|
{
|
|
question: "What devices can I connect to the VPN router?",
|
|
answer:
|
|
"Any device that connects via Wi-Fi can use the VPN router, including Apple TV, Roku, Amazon Fire TV, gaming consoles, smart TVs, and computers. Simply connect to the VPN router's Wi-Fi network to route your traffic through the VPN server.",
|
|
},
|
|
{
|
|
question: "Can I use VPN on my phone or laptop directly?",
|
|
answer:
|
|
"The VPN router service is designed for devices that don't natively support VPN apps. For phones and laptops, you could connect them to the VPN router's Wi-Fi, but we recommend using a standard VPN app on those devices for better flexibility.",
|
|
},
|
|
{
|
|
question: "What internet speeds can I expect through the VPN?",
|
|
answer:
|
|
"VPN speeds depend on your base internet connection and the distance to the VPN server. Typically, you can expect 20-100 Mbps for streaming, which is sufficient for 4K content. The San Francisco server generally offers faster speeds for users in Japan.",
|
|
},
|
|
{
|
|
question: "Can I switch between regions after signing up?",
|
|
answer:
|
|
"Each router is pre-configured for one region (San Francisco or London). If you need to access content from both regions, you would need two separate router rentals. Contact us if you need to change your region assignment.",
|
|
},
|
|
{
|
|
question: "What happens if the VPN router breaks or stops working?",
|
|
answer:
|
|
"We provide technical support and will replace faulty equipment at no extra charge. Simply contact our support team and we'll arrange a replacement router to be shipped to you.",
|
|
},
|
|
];
|
|
|
|
function VpnFaqSection() {
|
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
|
|
return (
|
|
<div className="bg-card rounded-xl border border-border p-8 mb-8">
|
|
<h2 className="text-xl font-bold text-foreground mb-6">Frequently Asked Questions</h2>
|
|
<div className="space-y-0 divide-y divide-border">
|
|
{vpnFaqItems.map((item, index) => (
|
|
<div key={index} className="py-4 first:pt-0 last:pb-0">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpenIndex(openIndex === index ? null : index)}
|
|
className="w-full flex items-start justify-between gap-3 text-left hover:text-primary transition-colors"
|
|
>
|
|
<span className="font-medium text-foreground">{item.question}</span>
|
|
{openIndex === index ? (
|
|
<ChevronUp className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
|
) : (
|
|
<ChevronDown className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
|
)}
|
|
</button>
|
|
{openIndex === index && (
|
|
<p className="mt-3 text-sm text-muted-foreground leading-relaxed">{item.answer}</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface HowItWorksStepProps {
|
|
number: number;
|
|
icon: React.ReactNode;
|
|
title: string;
|
|
description: string;
|
|
}
|
|
|
|
function HowItWorksStep({ number, icon, title, description }: HowItWorksStepProps) {
|
|
return (
|
|
<div className="flex flex-col items-center text-center flex-1 min-w-0">
|
|
{/* Icon with number badge */}
|
|
<div className="relative mb-4">
|
|
<div className="flex h-16 w-16 items-center justify-center rounded-xl bg-gray-50 border border-gray-200 text-primary shadow-sm">
|
|
{icon}
|
|
</div>
|
|
{/* Number badge */}
|
|
<div className="absolute -top-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-white text-xs font-bold shadow-sm">
|
|
{number}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<h4 className="font-semibold text-foreground mb-2">{title}</h4>
|
|
<p className="text-sm text-muted-foreground leading-relaxed max-w-[180px]">{description}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function VpnHowItWorksSection() {
|
|
const steps = [
|
|
{
|
|
icon: <CreditCard className="h-6 w-6" />,
|
|
title: "Sign Up",
|
|
description: "Create your account to get started",
|
|
},
|
|
{
|
|
icon: <Globe className="h-6 w-6" />,
|
|
title: "Choose Region",
|
|
description: "Select US (San Francisco) or UK (London)",
|
|
},
|
|
{
|
|
icon: <Package className="h-6 w-6" />,
|
|
title: "Place Order",
|
|
description: "Complete checkout and receive router",
|
|
},
|
|
{
|
|
icon: <Play className="h-6 w-6" />,
|
|
title: "Connect & Stream",
|
|
description: "Plug in, connect devices, enjoy",
|
|
},
|
|
];
|
|
|
|
return (
|
|
<section className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] p-8 mb-8">
|
|
{/* Header */}
|
|
<div className="text-center mb-8">
|
|
<span className="text-sm font-semibold text-primary uppercase tracking-wider">
|
|
Simple Setup
|
|
</span>
|
|
<h3 className="text-2xl font-bold text-foreground mt-1">How It Works</h3>
|
|
</div>
|
|
|
|
{/* Steps with connecting line */}
|
|
<div className="relative">
|
|
{/* Connecting line - hidden on mobile */}
|
|
<div className="hidden md:block absolute top-8 left-[12%] right-[12%] h-0.5 bg-gray-200" />
|
|
|
|
{/* Curved path SVG for visual connection - hidden on mobile */}
|
|
<svg
|
|
className="hidden md:block absolute top-[30px] left-0 right-0 w-full h-4 pointer-events-none"
|
|
preserveAspectRatio="none"
|
|
>
|
|
<path
|
|
d="M 12% 8 Q 30% 8, 37.5% 8 Q 45% 8, 50% 8 Q 55% 8, 62.5% 8 Q 70% 8, 88% 8"
|
|
fill="none"
|
|
stroke="#e5e7eb"
|
|
strokeWidth="2"
|
|
strokeDasharray="6 4"
|
|
/>
|
|
</svg>
|
|
|
|
{/* Steps grid */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 relative z-10">
|
|
{steps.map((step, index) => (
|
|
<HowItWorksStep
|
|
key={index}
|
|
number={index + 1}
|
|
icon={step.icon}
|
|
title={step.title}
|
|
description={step.description}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export default PublicVpnPlansView;
|