style: enhance layout and design of service components

- Updated ServicesPage layout by removing unnecessary padding for a cleaner appearance.
- Added a "Contact" link to the PublicShell navigation for improved accessibility.
- Refined AboutUsView by restructuring service and value data for better clarity and organization.
- Improved styling consistency across various service components, including ServiceCTA, ServiceFAQ, and ServiceHighlights, by adjusting spacing and typography.
- Enhanced InternetOfferingCard and PublicOfferingCard with updated tier styles and visual hierarchy for better user experience.
This commit is contained in:
barsa 2026-03-04 18:57:30 +09:00
parent d5294dc580
commit 48eb8c8725
16 changed files with 1197 additions and 1270 deletions

View File

@ -22,7 +22,7 @@ export const metadata: Metadata = {
export default function ServicesPage() {
return (
<div className="max-w-6xl mx-auto px-4 pt-8">
<div className="max-w-6xl mx-auto">
<ServicesOverviewContent basePath="/services" />
</div>
);

View File

@ -261,6 +261,12 @@ export function PublicShell({ children }: PublicShellProps) {
>
Support
</Link>
<Link
href="/contact"
className="inline-flex items-center px-3 py-2 rounded-md hover:text-foreground transition-colors"
>
Contact
</Link>
</nav>
{/* Spacer for mobile only - takes center column when nav is hidden */}
@ -396,6 +402,13 @@ export function PublicShell({ children }: PublicShellProps) {
>
Support
</Link>
<Link
href="/contact"
onClick={closeMobileMenu}
className="flex items-center px-3 py-3.5 rounded-xl text-base font-semibold text-foreground hover:bg-muted/50 active:bg-muted/70 transition-colors"
>
Contact
</Link>
</div>
</div>

View File

@ -1,7 +1,5 @@
"use client";
import Image from "next/image";
import { useCallback, useEffect, useRef, useState } from "react";
import type { LucideIcon } from "lucide-react";
import {
Wifi,
Smartphone,
@ -13,349 +11,342 @@ import {
Lightbulb,
Globe,
Shield,
Quote,
MapPin,
Phone,
Users,
Calendar,
Banknote,
BriefcaseBusiness,
} from "lucide-react";
/**
* AboutUsView - Corporate profile and company information
*
* Displays company background, corporate data, business activities,
* and mission statement for Assist Solutions.
*/
export function AboutUsView() {
const values = [
{
text: "Make technology accessible for everyone, regardless of language barriers.",
icon: <Heart className="h-6 w-6" />,
color: "bg-rose-50 text-rose-500 border-rose-100",
},
{
text: "Save our customers time by handling Japanese bureaucracy and paperwork for them.",
icon: <Clock3 className="h-6 w-6" />,
color: "bg-amber-50 text-amber-500 border-amber-100",
},
{
text: "Stay current with the latest technology to provide the best solutions for our clients.",
icon: <Lightbulb className="h-6 w-6" />,
color: "bg-sky-50 text-sky-500 border-sky-100",
},
{
text: "Be a bridge between Japan's tech infrastructure and its international community.",
icon: <Globe className="h-6 w-6" />,
color: "bg-emerald-50 text-emerald-500 border-emerald-100",
},
{
text: "Operate with transparency and integrity in all our customer relationships.",
icon: <Shield className="h-6 w-6" />,
color: "bg-violet-50 text-violet-500 border-violet-100",
},
];
/* ─── Data ─── */
const services = [
{
title: "Internet Plans",
description:
"High-speed NTT fiber with English support. We handle the Japanese paperwork so you don't have to.",
icon: <Wifi className="h-7 w-7 text-primary" />,
},
{
title: "Phone Plans",
description:
"SIM cards on Japan's best network. Foreign credit cards accepted, no hanko required.",
icon: <Smartphone className="h-7 w-7 text-primary" />,
},
{
title: "Business Solutions",
description:
"Enterprise IT for international companies. Dedicated internet, office networks, and data centers.",
icon: <Building2 className="h-7 w-7 text-primary" />,
},
{
title: "VPN",
description: "Stream your favorite shows from home. Pre-configured router for US/UK content.",
icon: <Lock className="h-7 w-7 text-primary" />,
},
{
title: "Onsite Support",
description: "English-speaking technicians at your door for setup and troubleshooting.",
icon: <Wrench className="h-7 w-7 text-primary" />,
},
];
const services: { title: string; description: string; icon: LucideIcon }[] = [
{
title: "Internet Plans",
description:
"High-speed NTT fiber with English support. We handle the Japanese paperwork so you don't have to.",
icon: Wifi,
},
{
title: "Phone Plans",
description:
"SIM cards on Japan's best network. Foreign credit cards accepted, no hanko required.",
icon: Smartphone,
},
{
title: "Business Solutions",
description:
"Enterprise IT for international companies. Dedicated internet, office networks, and data centers.",
icon: Building2,
},
{
title: "VPN",
description: "Stream your favorite shows from home. Pre-configured router for US/UK content.",
icon: Lock,
},
{
title: "Onsite Support",
description: "English-speaking technicians at your door for setup and troubleshooting.",
icon: Wrench,
},
];
const carouselRef = useRef<HTMLDivElement>(null);
const [scrollAmount, setScrollAmount] = useState(0);
const values: {
title: string;
text: string;
icon: LucideIcon;
accent: string;
}[] = [
{
title: "Accessibility",
text: "Make technology accessible for everyone, regardless of language barriers.",
icon: Heart,
accent: "text-rose-500 bg-rose-500/10",
},
{
title: "Time-Saving",
text: "Save our customers time by handling Japanese bureaucracy and paperwork for them.",
icon: Clock3,
accent: "text-amber-500 bg-amber-500/10",
},
{
title: "Innovation",
text: "Stay current with the latest technology to provide the best solutions for our clients.",
icon: Lightbulb,
accent: "text-sky-500 bg-sky-500/10",
},
{
title: "Connection",
text: "Be a bridge between Japan's tech infrastructure and its international community.",
icon: Globe,
accent: "text-emerald-500 bg-emerald-500/10",
},
{
title: "Integrity",
text: "Operate with transparency and integrity in all our customer relationships.",
icon: Shield,
accent: "text-violet-500 bg-violet-500/10",
},
];
const computeScrollAmount = useCallback(() => {
const container = carouselRef.current;
if (!container) return;
const card = container.querySelector<HTMLElement>("[data-business-card]");
if (!card) return;
const gap =
Number.parseFloat(getComputedStyle(container).columnGap || "0") ||
Number.parseFloat(getComputedStyle(container).gap || "0") ||
24;
setScrollAmount(card.clientWidth + gap);
}, []);
const corporateDetails: { label: string; value: string; icon: LucideIcon }[] = [
{
label: "Representative Director",
value: "Daisuke Nagakawa",
icon: Users,
},
{
label: "Employees",
value: "21 Staff Members (as of March 31st, 2025)",
icon: BriefcaseBusiness,
},
{ label: "Established", value: "March 8, 2002", icon: Calendar },
{ label: "Paid in Capital", value: "40,000,000 JPY", icon: Banknote },
];
const scrollServices = useCallback(
(direction: 1 | -1) => {
const container = carouselRef.current;
if (!container) return;
const amount = scrollAmount || container.clientWidth;
container.scrollBy({ left: direction * amount, behavior: "smooth" });
},
[scrollAmount]
);
const businessHours = [
{ team: "Customer Support Team", hours: "Mon \u2013 Fri 9:30AM \u2013 6:00PM" },
{ team: "In-office Tech Support Team", hours: "Mon \u2013 Fri 9:30AM \u2013 6:00PM" },
{ team: "Onsite Tech Support Team", hours: "Mon \u2013 Sat 10:00AM \u2013 9:00PM" },
];
useEffect(() => {
computeScrollAmount();
window.addEventListener("resize", computeScrollAmount);
return () => window.removeEventListener("resize", computeScrollAmount);
}, [computeScrollAmount]);
/* ─── Full-width section wrapper ─── */
const SECTION_BASE = "relative left-1/2 right-1/2 w-screen -translate-x-1/2";
const HERO_GLOW_STYLE = {
background: "radial-gradient(circle, oklch(0.7 0.12 234.4 / 0.35), transparent 70%)",
} as const;
/* ─── Section components ─── */
function HeroSection() {
return (
<div className="space-y-0">
{/* Hero with geometric pattern */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-gradient-to-br from-slate-50 to-sky-50/30 py-12 sm:py-16 overflow-hidden">
{/* Dot grid pattern */}
<div
className="absolute inset-0 opacity-[0.4]"
style={{
backgroundImage: `radial-gradient(circle, #0ea5e9 1px, transparent 1px)`,
backgroundSize: "24px 24px",
}}
/>
{/* Subtle gradient overlay for depth */}
<div className="absolute inset-0 bg-gradient-to-t from-white/80 via-transparent to-white/40" />
<div className="relative max-w-6xl mx-auto px-6 sm:px-8">
<div className="grid grid-cols-1 lg:grid-cols-[1.05fr_minmax(0,0.95fr)] gap-10 items-center">
<div className="space-y-5">
<h1 className="text-4xl sm:text-5xl font-extrabold text-primary leading-tight tracking-tight">
About Us
</h1>
<div className="space-y-4 text-muted-foreground leading-relaxed text-base sm:text-lg">
<p>
Since 2002, Assist Solutions has been the trusted IT partner for expats and
international businesses in Japan. We understand the unique challenges of living
and working in a country where language barriers can make simple tasks difficult.
</p>
<p>
Our bilingual team provides internet, mobile, VPN, and tech support services with
full English support. No Japanese required. We handle everything from contracts to
installation coordination, so you can focus on enjoying life in Japan.
</p>
</div>
</div>
<div className="relative h-full min-h-[420px]">
<Image
src="/assets/images/About us.png"
alt="Assist Solutions team in Tokyo"
fill
priority
className="object-contain drop-shadow-lg"
sizes="(max-width: 1024px) 100vw, 45vw"
/>
</div>
</div>
</div>
{/* Gradient fade to Business Solutions section */}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-[#e8f5ff] pointer-events-none z-10" />
</section>
{/* Business Solutions Carousel */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#e8f5ff] py-10">
{/* Gradient fade to next section */}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-white pointer-events-none" />
<div className="mx-auto max-w-6xl px-6 sm:px-10">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl sm:text-3xl font-bold text-foreground">Business</h2>
</div>
<div className="relative">
<div
ref={carouselRef}
className="flex gap-6 overflow-x-auto scroll-smooth pb-6 pr-20"
style={{ scrollbarWidth: "none" }}
>
{services.map((service, idx) => (
<article
key={`${service.title}-${idx}`}
data-business-card
className="flex-shrink-0 w-[240px] rounded-3xl bg-white px-6 py-7 shadow-md border border-white/60"
>
<div className="mb-4">{service.icon}</div>
<h3 className="text-lg font-semibold text-foreground mb-2">{service.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{service.description}
</p>
</article>
))}
</div>
<div className="absolute bottom-0 right-2 flex gap-3">
<button
type="button"
onClick={() => scrollServices(-1)}
className="h-9 w-9 rounded-full border border-black/15 bg-white text-foreground shadow-sm hover:bg-white/90"
aria-label="Scroll business left"
>
</button>
<button
type="button"
onClick={() => scrollServices(1)}
className="h-9 w-9 rounded-full border border-black/15 bg-white text-foreground shadow-sm hover:bg-white/90"
aria-label="Scroll business right"
>
</button>
</div>
</div>
</div>
</section>
{/* Our Values Section */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-12 sm:py-14">
{/* Gradient fade to next section */}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-[#f7f7f7] pointer-events-none" />
<div className="max-w-6xl mx-auto px-6 sm:px-8 space-y-8">
<div className="text-center max-w-2xl mx-auto">
<h2 className="text-2xl sm:text-3xl font-bold text-primary mb-3">Our Values</h2>
<p className="text-muted-foreground leading-relaxed">
These principles guide how we serve customers, support our community, and advance our
craft every day.
</p>
</div>
{/* Values Grid - 3 on top, 2 centered on bottom */}
<div className="space-y-4">
{/* Top row - 3 cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{values.slice(0, 3).map((value, index) => (
<div
key={index}
className="group relative bg-white rounded-2xl border border-border/60 p-6 shadow-sm hover:shadow-md hover:-translate-y-1 transition-all duration-300"
>
{/* Icon */}
<div
className={`inline-flex items-center justify-center w-12 h-12 rounded-xl border mb-4 ${value.color}`}
>
{value.icon}
</div>
{/* Quote mark decoration */}
<Quote className="absolute top-4 right-4 h-8 w-8 text-muted-foreground/10 rotate-180" />
{/* Text */}
<p className="text-foreground font-medium leading-relaxed">{value.text}</p>
</div>
))}
</div>
{/* Bottom row - 2 cards centered */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-2xl mx-auto lg:max-w-none lg:grid-cols-2 lg:px-[16.666%]">
{values.slice(3).map((value, index) => (
<div
key={index + 3}
className="group relative bg-white rounded-2xl border border-border/60 p-6 shadow-sm hover:shadow-md hover:-translate-y-1 transition-all duration-300"
>
{/* Icon */}
<div
className={`inline-flex items-center justify-center w-12 h-12 rounded-xl border mb-4 ${value.color}`}
>
{value.icon}
</div>
{/* Quote mark decoration */}
<Quote className="absolute top-4 right-4 h-8 w-8 text-muted-foreground/10 rotate-180" />
{/* Text */}
<p className="text-foreground font-medium leading-relaxed">{value.text}</p>
</div>
))}
</div>
</div>
</div>
</section>
{/* Corporate Data Section */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-12 pb-16">
<div className="max-w-6xl mx-auto px-6 sm:px-8 space-y-3">
{/* Row 1: headings same level */}
<div className="grid grid-cols-1 lg:grid-cols-[1.2fr_0.8fr] gap-x-10 gap-y-2 items-start">
<h2 className="text-2xl font-bold text-foreground">Corporate Data</h2>
<div>
<h3 className="text-xl font-bold text-foreground mb-2">Address</h3>
<p className="text-muted-foreground font-semibold leading-relaxed">
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu,
<br />
Minato-ku, Tokyo 106-0044
<br />
Tel: 03-3560-1006 Fax: 03-3560-1007
<section className={`${SECTION_BASE} overflow-hidden bg-surface-sunken`}>
<div
className="pointer-events-none absolute -top-1/3 right-0 h-[600px] w-[600px] opacity-30"
style={HERO_GLOW_STYLE}
/>
<div className="relative mx-auto max-w-6xl px-6 py-16 sm:px-8 sm:py-20 lg:py-24">
<div className="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div className="cp-stagger-children space-y-6">
<span className="inline-block rounded-full bg-primary/10 px-4 py-1.5 text-sm font-semibold tracking-wide text-primary">
Since 2002
</span>
<h1 className="text-display-lg font-extrabold leading-[1.1] tracking-tight font-display text-foreground">
Your Trusted IT Partner <span className="cp-gradient-text">in Japan</span>
</h1>
<div className="max-w-lg space-y-4 text-base leading-relaxed text-muted-foreground sm:text-lg">
<p>
Assist Solutions has been the go-to IT partner for expats and international
businesses in Japan for over two decades. We understand the unique challenges of
living and working where language barriers can make simple tasks difficult.
</p>
<p>
Our bilingual team provides internet, mobile, VPN, and tech support with full
English service. No Japanese required we handle everything from contracts to
installation.
</p>
</div>
</div>
{/* Row 2: corporate data list | map (no stretch, no extra space below left column) */}
<div className="grid grid-cols-1 lg:grid-cols-[1.2fr_0.8fr] gap-x-10 gap-y-2 items-start">
<div className="space-y-2">
<div>
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
<span className="h-6 w-1 rounded-full bg-primary" />
Representative Director
</h3>
<p className="text-muted-foreground font-semibold mt-0.5">Daisuke Nagakawa</p>
</div>
<div>
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
<span className="h-6 w-1 rounded-full bg-primary" />
Employees
</h3>
<p className="text-muted-foreground font-semibold mt-0.5">
21 Staff Members (as of March 31st, 2025)
</p>
</div>
<div>
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
<span className="h-6 w-1 rounded-full bg-primary" />
Established
</h3>
<p className="text-muted-foreground font-semibold mt-0.5">March 8, 2002</p>
</div>
<div>
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
<span className="h-6 w-1 rounded-full bg-primary" />
Paid in Capital
</h3>
<p className="text-muted-foreground font-semibold mt-0.5">40,000,000 JPY</p>
</div>
<div>
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
<span className="h-6 w-1 rounded-full bg-primary" />
Business Hours
</h3>
<div className="text-muted-foreground font-semibold mt-0.5 space-y-0.5">
<p>Mon - Fri 9:30AM - 6:00PM Customer Support Team</p>
<p>Mon - Fri 9:30AM - 6:00PM In-office Tech Support Team</p>
<p>Mon - Sat 10:00AM - 9:00PM Onsite Tech Support Team</p>
</div>
</div>
</div>
<div className="rounded-2xl overflow-hidden w-full min-h-[320px]">
<iframe
title="Assist Solutions Corp Map"
src="https://www.google.com/maps?q=Assist+Solutions+Corp,+3-8-2+Higashi+Azabu,+Minato-ku,+Tokyo&output=embed"
className="w-full h-[320px] block"
loading="lazy"
allowFullScreen
referrerPolicy="no-referrer-when-downgrade"
/>
</div>
<div className="relative mx-auto h-[380px] w-full max-w-md lg:h-[440px] lg:max-w-none">
<Image
src="/assets/images/About us.png"
alt="Assist Solutions team in Tokyo"
fill
priority
className="object-contain drop-shadow-lg"
sizes="(max-width: 1024px) 90vw, 45vw"
/>
</div>
</div>
</section>
</div>
</section>
);
}
function ServicesSection() {
return (
<section className={`${SECTION_BASE} bg-background py-16 sm:py-20`}>
<div className="mx-auto max-w-6xl px-6 sm:px-8">
<div className="cp-stagger-children mb-10 max-w-xl">
<h2 className="text-display-sm font-bold font-display text-foreground">What We Do</h2>
<p className="mt-3 leading-relaxed text-muted-foreground">
End-to-end IT services designed for the international community in Japan all in
English.
</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{services.map(service => {
const Icon = service.icon;
return (
<div
key={service.title}
className="group cp-card-hover-lift rounded-2xl border border-border/60 bg-card p-6 hover:border-primary/20"
>
<div className="mb-4 inline-flex h-11 w-11 items-center justify-center rounded-xl bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
<Icon className="h-5 w-5" />
</div>
<h3 className="text-lg font-semibold text-foreground">{service.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
{service.description}
</p>
</div>
);
})}
</div>
</div>
</section>
);
}
function ValuesSection() {
return (
<section className={`${SECTION_BASE} bg-surface-sunken py-16 sm:py-20`}>
<div className="mx-auto max-w-6xl px-6 sm:px-8">
<div className="cp-stagger-children mb-10 text-center">
<h2 className="text-display-sm font-bold font-display text-foreground">Our Values</h2>
<p className="mx-auto mt-3 max-w-lg leading-relaxed text-muted-foreground">
These principles guide how we serve customers, support our community, and advance our
craft every day.
</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
{values.map(value => {
const Icon = value.icon;
return (
<div
key={value.title}
className="group cp-card-hover-lift rounded-2xl border border-border/40 bg-card p-5"
>
<div
className={`mb-3 inline-flex h-10 w-10 items-center justify-center rounded-lg ${value.accent}`}
>
<Icon className="h-5 w-5" />
</div>
<h3 className="mb-1.5 text-sm font-bold uppercase tracking-wide text-foreground/80">
{value.title}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">{value.text}</p>
</div>
);
})}
</div>
</div>
</section>
);
}
function CorporateSection() {
return (
<section className={`${SECTION_BASE} bg-background py-16 sm:py-20`}>
<div className="mx-auto max-w-6xl px-6 sm:px-8">
<div className="cp-stagger-children mb-10">
<h2 className="text-display-sm font-bold font-display text-foreground">Corporate Data</h2>
</div>
<div className="grid grid-cols-1 gap-10 lg:grid-cols-5">
<CorporateDetails />
<CorporateMap />
</div>
</div>
</section>
);
}
function CorporateDetails() {
return (
<div className="space-y-8 lg:col-span-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{corporateDetails.map(item => {
const Icon = item.icon;
return (
<div
key={item.label}
className="flex items-start gap-3 rounded-xl border border-border/40 bg-card p-4"
>
<div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-primary">
<Icon className="h-4 w-4" />
</div>
<div className="min-w-0">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{item.label}
</p>
<p className="mt-0.5 font-semibold text-foreground">{item.value}</p>
</div>
</div>
);
})}
</div>
<div className="rounded-xl border border-border/40 bg-card p-5">
<h3 className="mb-3 text-sm font-bold uppercase tracking-wider text-foreground/80">
Business Hours
</h3>
<div className="space-y-2">
{businessHours.map(item => (
<div key={item.team} className="flex items-center justify-between gap-4 text-sm">
<span className="text-muted-foreground">{item.team}</span>
<span className="shrink-0 font-medium text-foreground">{item.hours}</span>
</div>
))}
</div>
</div>
<div className="flex items-start gap-3">
<MapPin className="mt-1 h-5 w-5 shrink-0 text-primary" />
<div>
<p className="font-semibold text-foreground">
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu
</p>
<p className="text-muted-foreground">Minato-ku, Tokyo 106-0044</p>
<p className="mt-1 flex items-center gap-1.5 text-sm text-muted-foreground">
<Phone className="h-3.5 w-3.5" />
<a href="tel:03-3560-1006" className="transition-colors hover:text-primary">
03-3560-1006
</a>
</p>
</div>
</div>
</div>
);
}
export default AboutUsView;
function CorporateMap() {
return (
<div className="lg:col-span-2">
<div className="sticky top-24 overflow-hidden rounded-2xl border border-border/40 shadow-sm">
<iframe
title="Assist Solutions Corp Map"
src="https://www.google.com/maps?q=Assist+Solutions+Corp,+3-8-2+Higashi+Azabu,+Minato-ku,+Tokyo&output=embed"
className="block h-[360px] w-full lg:h-[420px]"
loading="lazy"
allowFullScreen
referrerPolicy="no-referrer-when-downgrade"
/>
</div>
</div>
);
}
/* ─── Main view ─── */
/**
* AboutUsView - Corporate profile and company information
*
* Displays company background, services, values, and corporate data
* for Assist Solutions.
*/
export function AboutUsView() {
return (
<div>
<HeroSection />
<ServicesSection />
<ValuesSection />
<CorporateSection />
</div>
);
}

View File

@ -17,10 +17,8 @@ export interface HowItWorksProps {
}
/**
* HowItWorks - Visual step-by-step process component
*
* Displays a numbered process with icons and descriptions.
* Horizontal layout on desktop, vertical stack on mobile.
* HowItWorks - Visual step-by-step process.
* Horizontal on desktop, vertical stack on mobile.
*/
export function HowItWorks({
title = "How It Works",
@ -29,50 +27,46 @@ export function HowItWorks({
className,
}: HowItWorksProps) {
return (
<section className={cn("py-8", className)}>
{/* Section Header */}
<section className={cn("py-6", className)}>
{/* Header */}
<div className="text-center mb-8">
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-2">
<p className="text-xs font-semibold text-primary uppercase tracking-wider mb-1.5">
{eyebrow}
</p>
<h2 className="text-2xl sm:text-3xl font-display font-semibold leading-tight tracking-tight text-foreground">
<h2 className="text-xl sm:text-2xl font-bold leading-tight tracking-tight text-foreground">
{title}
</h2>
</div>
{/* Steps Container */}
{/* Steps */}
<div className="relative">
{/* Connection line - visible on md+ */}
{/* Connection line — desktop */}
<div
className="hidden md:block absolute top-8 left-[10%] right-[10%] h-0.5 bg-gradient-to-r from-transparent via-border to-transparent"
className="hidden md:block absolute top-8 left-[12%] right-[12%] h-px bg-border"
aria-hidden="true"
/>
{/* Steps Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-4">
{steps.map((step, index) => (
<div key={index} className="relative flex flex-col items-center text-center group">
{/* Step Number Badge */}
<div className="relative mb-4">
{/* Icon Container */}
<div className="w-16 h-16 rounded-2xl bg-card border border-border shadow-sm flex items-center justify-center text-primary group-hover:border-primary/40 group-hover:shadow-md transition-all duration-[var(--cp-duration-normal)]">
{/* Icon */}
<div className="relative mb-3">
<div className="w-14 h-14 rounded-xl bg-card border border-border shadow-sm flex items-center justify-center text-primary group-hover:border-primary/30 group-hover:shadow-md transition-all duration-200">
{step.icon}
</div>
{/* Number Badge */}
<div className="absolute -top-2 -right-2 w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs font-bold flex items-center justify-center shadow-sm">
<div className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-primary text-primary-foreground text-[10px] font-bold flex items-center justify-center">
{index + 1}
</div>
</div>
{/* Content */}
<h3 className="text-base font-semibold text-foreground mb-1.5">{step.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed max-w-[200px]">
<h3 className="text-sm font-semibold text-foreground mb-1">{step.title}</h3>
<p className="text-xs text-muted-foreground leading-relaxed max-w-[180px]">
{step.description}
</p>
{/* Vertical connector for mobile - between steps */}
{/* Vertical connector — mobile */}
{index < steps.length - 1 && (
<div className="md:hidden w-0.5 h-6 bg-border mt-4" aria-hidden="true" />
<div className="md:hidden w-px h-5 bg-border mt-3" aria-hidden="true" />
)}
</div>
))}

View File

@ -25,10 +25,7 @@ export interface ServiceCTAProps {
}
/**
* ServiceCTA - Reusable call-to-action section
*
* Gradient background CTA with decorative elements.
* Used at the bottom of service pages to drive conversion.
* ServiceCTA - Call-to-action section with decorative background.
*/
export function ServiceCTA({
eyebrow = "Get started in minutes",
@ -40,38 +37,32 @@ export function ServiceCTA({
className,
}: ServiceCTAProps) {
return (
<section className={cn("relative text-center py-12 rounded-2xl overflow-hidden", className)}>
<section className={cn("relative text-center py-10 rounded-2xl overflow-hidden", className)}>
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 via-primary/5 to-purple-500/10" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_color-mix(in_oklch,var(--color-indigo-500)_10%,transparent),_transparent_70%)]" />
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-primary/8" />
{/* Decorative rings */}
{/* Refined dot pattern */}
<div
className="absolute top-4 left-8 w-20 h-20 rounded-full border border-primary/10 cp-float hidden sm:block"
aria-hidden="true"
/>
<div
className="absolute bottom-4 right-8 w-16 h-16 rounded-full border border-blue-500/10 cp-float-delayed hidden sm:block"
aria-hidden="true"
className="absolute inset-0 pointer-events-none opacity-30"
style={{
backgroundImage: `radial-gradient(circle at center, color-mix(in oklch, var(--primary) 12%, transparent) 0.6px, transparent 0.6px)`,
backgroundSize: "20px 20px",
}}
/>
{/* Content */}
<div className="relative">
{/* Eyebrow */}
<div className="inline-flex items-center gap-1.5 text-xs font-medium text-primary mb-4 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20">
<div className="inline-flex items-center gap-1.5 text-xs font-medium text-primary mb-3 px-2.5 py-1 rounded-full bg-primary/10 border border-primary/15">
{eyebrowIcon}
{eyebrow}
</div>
{/* Headline */}
<h2 className="text-2xl sm:text-3xl font-display font-semibold leading-tight tracking-tight text-foreground mb-3">
<h2 className="text-xl sm:text-2xl font-bold leading-tight tracking-tight text-foreground mb-2">
{headline}
</h2>
{/* Description */}
<p className="text-base text-muted-foreground mb-6 max-w-md mx-auto">{description}</p>
<p className="text-sm text-muted-foreground mb-5 max-w-md mx-auto">{description}</p>
{/* Actions */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
{primaryAction.onClick ? (
<Button

View File

@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { ChevronDown } from "lucide-react";
import { cn } from "@/shared/utils/cn";
export interface FAQItem {
@ -17,9 +17,6 @@ export interface ServiceFAQProps {
defaultOpenIndex?: number | null;
}
/**
* Individual FAQ item with expand/collapse
*/
function FAQItemComponent({
question,
answer,
@ -32,19 +29,22 @@ function FAQItemComponent({
onToggle: () => void;
}) {
return (
<div className="border-b border-border last:border-b-0">
<div className="border-b border-border/60 last:border-b-0">
<button
type="button"
onClick={onToggle}
className="w-full py-4 flex items-start justify-between gap-3 text-left hover:text-primary transition-colors"
className="w-full py-4 flex items-start justify-between gap-3 text-left group"
aria-expanded={isOpen}
>
<span className="text-sm font-medium text-foreground">{question}</span>
{isOpen ? (
<ChevronUp className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-0.5" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-0.5" />
)}
<span className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
{question}
</span>
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200",
isOpen && "rotate-180"
)}
/>
</button>
<div
className={cn(
@ -61,10 +61,7 @@ function FAQItemComponent({
}
/**
* ServiceFAQ - Reusable FAQ accordion section
*
* Displays frequently asked questions with collapsible answers.
* Used at the bottom of service pages to address common concerns.
* ServiceFAQ - FAQ accordion section.
*/
export function ServiceFAQ({
title = "Frequently Asked Questions",
@ -78,19 +75,19 @@ export function ServiceFAQ({
if (items.length === 0) return null;
return (
<section className={cn("py-8", className)}>
{/* Section Header */}
<section className={cn("py-6", className)}>
{/* Header */}
<div className="text-center mb-6">
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-2">
<p className="text-xs font-semibold text-primary uppercase tracking-wider mb-1.5">
{eyebrow}
</p>
<h2 className="text-2xl sm:text-3xl font-display font-semibold leading-tight tracking-tight text-foreground">
<h2 className="text-xl sm:text-2xl font-bold leading-tight tracking-tight text-foreground">
{title}
</h2>
</div>
{/* FAQ Container */}
<div className="bg-card border border-border rounded-2xl px-5 shadow-sm">
{/* FAQ list */}
<div className="bg-card border border-border rounded-xl px-5 shadow-sm">
{items.map((item, index) => (
<FAQItemComponent
key={index}

View File

@ -1,7 +1,6 @@
"use client";
import { useRef, useState, useEffect } from "react";
import { CheckCircle } from "lucide-react";
import { cn } from "@/shared/utils";
export interface HighlightFeature {
@ -18,16 +17,15 @@ interface ServiceHighlightsProps {
function HighlightItem({ icon, title, description, highlight }: HighlightFeature) {
return (
<div className="group relative flex items-start gap-3.5 p-4 rounded-xl border border-border/40 hover:bg-muted/40 transition-colors duration-200">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary flex-shrink-0 mt-0.5">
<div className="group relative flex items-start gap-3 p-4 rounded-xl bg-card border border-border/60 hover:border-primary/30 hover:shadow-sm transition-all duration-200">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/8 text-primary flex-shrink-0 mt-0.5">
{icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<div className="flex items-center gap-2 mb-0.5">
<h3 className="font-semibold text-foreground text-sm">{title}</h3>
{highlight && (
<span className="inline-flex items-center gap-1 py-0.5 px-2 rounded-full bg-primary/10 text-[10px] font-semibold text-primary whitespace-nowrap">
<CheckCircle className="h-3 w-3" />
<span className="inline-flex py-0.5 px-1.5 rounded-md bg-primary/8 text-[10px] font-semibold text-primary whitespace-nowrap">
{highlight}
</span>
)}
@ -38,26 +36,20 @@ function HighlightItem({ icon, title, description, highlight }: HighlightFeature
);
}
/**
* Mobile Carousel Item - Compact card for horizontal scrolling
*/
function MobileCarouselItem({ icon, title, description, highlight }: HighlightFeature) {
return (
<div className="flex-shrink-0 w-[280px] snap-center">
<div className="h-full p-4 rounded-xl bg-gradient-to-br from-muted/40 to-muted/20 border border-border/40 shadow-sm">
{/* Top row: Icon + Highlight badge */}
<div className="flex-shrink-0 w-[260px] snap-center">
<div className="h-full p-4 rounded-xl bg-card border border-border/60 shadow-sm">
<div className="flex items-center justify-between gap-2 mb-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary flex-shrink-0">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/8 text-primary flex-shrink-0">
{icon}
</div>
{highlight && (
<span className="inline-flex items-center gap-1 py-0.5 px-2 rounded-full bg-primary/10 text-[10px] font-bold text-primary uppercase tracking-wide">
<span className="inline-flex py-0.5 px-1.5 rounded-md bg-primary/8 text-[10px] font-bold text-primary uppercase tracking-wide">
{highlight}
</span>
)}
</div>
{/* Content */}
<h3 className="font-semibold text-foreground text-sm mb-1">{title}</h3>
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-3">{description}</p>
</div>
@ -66,24 +58,20 @@ function MobileCarouselItem({ icon, title, description, highlight }: HighlightFe
}
/**
* ServiceHighlights
*
* A clean, grid-based layout for displaying service features/highlights.
* On mobile: horizontal scrolling carousel with snap points.
* On desktop: grid layout.
* ServiceHighlights - Grid-based feature highlights.
* Mobile: horizontal scrolling carousel. Desktop: 3-column grid.
*/
export function ServiceHighlights({ features, className = "" }: ServiceHighlightsProps) {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [activeIndex, setActiveIndex] = useState(0);
// Track scroll position to update active dot indicator
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
const handleScroll = () => {
const scrollLeft = container.scrollLeft;
const itemWidth = 280 + 12; // card width + gap
const itemWidth = 260 + 12;
const newIndex = Math.min(Math.round(scrollLeft / itemWidth), features.length - 1);
setActiveIndex(prev => (prev === newIndex ? prev : newIndex));
};
@ -92,36 +80,29 @@ export function ServiceHighlights({ features, className = "" }: ServiceHighlight
return () => container.removeEventListener("scroll", handleScroll);
}, [features.length]);
// Scroll to specific item when dot is clicked
const scrollToIndex = (index: number) => {
const container = scrollContainerRef.current;
if (!container) return;
const itemWidth = 280 + 12; // card width + gap
container.scrollTo({
left: index * itemWidth,
behavior: "smooth",
});
const itemWidth = 260 + 12;
container.scrollTo({ left: index * itemWidth, behavior: "smooth" });
};
return (
<>
{/* Mobile: Horizontal scrolling carousel */}
{/* Mobile: Horizontal carousel */}
<div className={cn("md:hidden", className)}>
{/* Scroll container */}
<div
ref={scrollContainerRef}
className="flex gap-3 overflow-x-auto pb-4 -mx-4 px-4 snap-x snap-mandatory scrollbar-hide touch-pan-x"
className="flex gap-3 overflow-x-auto pb-3 -mx-4 px-4 snap-x snap-mandatory scrollbar-hide touch-pan-x"
>
{features.map((feature, index) => (
<MobileCarouselItem key={index} {...feature} />
))}
{/* End spacer for last item visibility */}
<div className="flex-shrink-0 w-1" aria-hidden="true" />
</div>
{/* Dot indicators */}
<div className="flex justify-center gap-1.5 mt-2">
<div className="flex justify-center gap-1.5 mt-1.5">
{features.map((_, index) => (
<button
key={index}
@ -131,20 +112,15 @@ export function ServiceHighlights({ features, className = "" }: ServiceHighlight
className={cn(
"h-1.5 rounded-full transition-all duration-300",
activeIndex === index
? "w-6 bg-primary"
: "w-1.5 bg-muted-foreground/30 hover:bg-muted-foreground/50"
? "w-5 bg-primary"
: "w-1.5 bg-muted-foreground/25 hover:bg-muted-foreground/40"
)}
/>
))}
</div>
{/* Swipe hint - only show initially */}
<p className="text-[10px] text-muted-foreground/60 text-center mt-2">
Swipe to explore features
</p>
</div>
{/* Desktop: Grid layout */}
{/* Desktop: Grid */}
<div className={cn("hidden md:grid md:grid-cols-2 lg:grid-cols-3 gap-3", className)}>
{features.map((feature, index) => (
<HighlightItem key={index} {...feature} />

View File

@ -38,16 +38,16 @@ export function ServicesHero({
return (
<div
className={cn(
"flex flex-col gap-3 mb-10",
"flex flex-col gap-2",
alignmentMap[align],
className,
align === "center" ? "mx-auto max-w-3xl" : ""
align === "center" ? "mx-auto max-w-2xl" : ""
)}
>
{eyebrow ? (
<div
className={cn(
"text-sm font-semibold text-primary uppercase tracking-wider",
"text-sm font-semibold text-primary uppercase tracking-wider mb-1",
animationClasses
)}
style={animated ? { animationDelay: "0ms" } : undefined}
@ -57,7 +57,7 @@ export function ServicesHero({
) : null}
<h1
className={cn(
"text-3xl sm:text-4xl lg:text-5xl text-foreground leading-tight font-extrabold",
"text-2xl sm:text-3xl lg:text-4xl text-foreground leading-tight font-bold tracking-tight",
displayFont && "font-display",
animationClasses
)}
@ -67,7 +67,7 @@ export function ServicesHero({
</h1>
<p
className={cn(
"text-base md:text-lg text-muted-foreground leading-relaxed max-w-xl",
"text-sm md:text-base text-muted-foreground leading-relaxed max-w-lg",
align === "center" && "mx-auto",
animationClasses
)}
@ -77,7 +77,7 @@ export function ServicesHero({
</p>
{children ? (
<div
className={cn("mt-2 w-full", animationClasses)}
className={cn("mt-1 w-full", animationClasses)}
style={animated ? { animationDelay: "150ms" } : undefined}
>
{children}

View File

@ -7,12 +7,8 @@ import {
ShieldCheck,
ArrowRight,
Phone,
CheckCircle2,
Globe,
Headphones,
Building2,
Wrench,
Zap,
Check,
} from "lucide-react";
@ -25,10 +21,37 @@ interface ServicesOverviewContentProps {
showCta?: boolean;
}
// Service data with enhanced information
const serviceAccents = {
internet: {
icon: "bg-sky-500/10 text-sky-600",
badge: "bg-sky-500/10 text-sky-700 border-sky-500/20",
stripe: "from-sky-500 to-blue-600",
},
sim: {
icon: "bg-emerald-500/10 text-emerald-600",
badge: "bg-emerald-500/10 text-emerald-700 border-emerald-500/20",
stripe: "from-emerald-500 to-teal-600",
},
vpn: {
icon: "bg-violet-500/10 text-violet-600",
badge: "bg-violet-500/10 text-violet-700 border-violet-500/20",
stripe: "from-violet-500 to-purple-600",
},
business: {
icon: "bg-slate-500/10 text-slate-600",
badge: "bg-slate-500/10 text-slate-700 border-slate-500/20",
stripe: "from-slate-500 to-slate-700",
},
onsite: {
icon: "bg-amber-500/10 text-amber-600",
badge: "bg-amber-500/10 text-amber-700 border-amber-500/20",
stripe: "from-amber-500 to-orange-600",
},
} as const;
const services = [
{
id: "internet",
id: "internet" as const,
icon: Wifi,
title: "Internet",
subtitle: "Fiber Optic",
@ -40,7 +63,7 @@ const services = [
useBasePath: true,
},
{
id: "sim",
id: "sim" as const,
icon: Smartphone,
title: "SIM & eSIM",
subtitle: "Mobile Data",
@ -53,7 +76,7 @@ const services = [
useBasePath: true,
},
{
id: "vpn",
id: "vpn" as const,
icon: ShieldCheck,
title: "VPN Router",
subtitle: "Streaming Access",
@ -65,7 +88,7 @@ const services = [
useBasePath: true,
},
{
id: "business",
id: "business" as const,
icon: Building2,
title: "Business",
subtitle: "Enterprise IT",
@ -76,7 +99,7 @@ const services = [
fixedPath: "/services/business",
},
{
id: "onsite",
id: "onsite" as const,
icon: Wrench,
title: "Onsite Support",
subtitle: "Tech Assistance",
@ -89,10 +112,10 @@ const services = [
];
/**
* ServicesOverviewContent - Enhanced services overview with rich visual design.
* ServicesOverviewContent - Services overview with clean, modern card design.
*
* Features full-width sections, gradient backgrounds, and polished card treatments
* matching the website's landing page aesthetic.
* Uses color-coded accents per service type and a refined layout
* with proper spacing and visual hierarchy.
*/
export function ServicesOverviewContent({
basePath,
@ -100,249 +123,221 @@ export function ServicesOverviewContent({
showCta = true,
}: ServicesOverviewContentProps) {
return (
<div className="relative">
{/* Hero Section with gradient background */}
<div>
{/* Hero Section */}
{showHero && (
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 overflow-hidden bg-gradient-to-br from-slate-50 via-white to-sky-50/80 pt-8 pb-16">
{/* Dot grid pattern */}
<section className="relative overflow-hidden pt-8 pb-12">
{/* Refined dot grid */}
<div
className="absolute inset-0 pointer-events-none"
className="absolute inset-0 pointer-events-none opacity-40"
style={{
backgroundImage: `radial-gradient(circle at center, color-mix(in oklch, var(--primary) 12%, transparent) 1px, transparent 1px)`,
backgroundSize: "24px 24px",
backgroundImage: `radial-gradient(circle at center, color-mix(in oklch, var(--primary) 10%, transparent) 0.8px, transparent 0.8px)`,
backgroundSize: "28px 28px",
}}
/>
{/* Gradient orb accent */}
{/* Gradient orb — top right, subtle */}
<div
className="absolute -top-32 -right-32 w-96 h-96 rounded-full pointer-events-none opacity-60"
className="absolute -top-20 -right-20 w-80 h-80 rounded-full pointer-events-none opacity-50"
style={{
background:
"radial-gradient(circle, color-mix(in oklch, var(--info) 30%, transparent) 0%, transparent 70%)",
"radial-gradient(circle, color-mix(in oklch, var(--info) 20%, transparent) 0%, transparent 65%)",
}}
/>
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Eyebrow badge */}
<div className="flex justify-center mb-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<span className="inline-flex items-center gap-2 rounded-full bg-white/80 backdrop-blur-sm border border-primary/20 px-4 py-2 text-sm text-primary font-medium shadow-sm">
<CheckCircle2 className="h-4 w-4" />
Full English Support
</span>
</div>
{/* Gradient orb — bottom left, complementary */}
<div
className="absolute -bottom-16 -left-16 w-64 h-64 rounded-full pointer-events-none opacity-30"
style={{
background:
"radial-gradient(circle, color-mix(in oklch, var(--primary) 18%, transparent) 0%, transparent 65%)",
}}
/>
{/* Main heading */}
<div className="relative max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-3 animate-in fade-in slide-in-from-bottom-4 duration-500">
Full English Support
</p>
<h1
className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-center text-foreground mb-6 tracking-tight animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "100ms" }}
className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground tracking-tight animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "80ms" }}
>
Our Services
</h1>
{/* Description */}
<p
className="text-lg sm:text-xl text-muted-foreground text-center max-w-2xl mx-auto mb-10 leading-relaxed animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "200ms" }}
className="text-base sm:text-lg text-muted-foreground mt-3 max-w-xl mx-auto leading-relaxed animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "160ms" }}
>
Connectivity and support solutions designed for Japan&apos;s international community.
One provider for all your needs.
</p>
{/* Value propositions */}
<div
className="flex flex-wrap justify-center gap-4 sm:gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "300ms" }}
>
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-border/40">
<Globe className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-foreground">
One provider, all services
</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-border/40">
<Headphones className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-foreground">English support</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-border/40">
<Zap className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-foreground">Fast activation</span>
</div>
</div>
</div>
{/* Gradient fade to services section */}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-[#f7f7f7] pointer-events-none" />
</section>
)}
{/* Services Section */}
<section
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] ${showHero ? "pt-8" : "pt-12"} pb-16`}
>
{/* Subtle pattern overlay */}
<div
className="absolute inset-0 pointer-events-none opacity-30"
style={{
backgroundImage: `radial-gradient(circle at center, color-mix(in oklch, var(--muted-foreground) 8%, transparent) 1px, transparent 1px)`,
backgroundSize: "32px 32px",
}}
/>
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section header */}
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl sm:text-3xl font-bold text-foreground">Choose Your Service</h2>
<div className="hidden sm:flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle2 className="h-4 w-4 text-primary" />
<span>No hidden fees</span>
</div>
</div>
{/* Featured services - Top row (Internet & SIM) */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Services Grid */}
<section className={showHero ? "" : "pt-2"}>
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Featured Services — Internet & SIM */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
{services.slice(0, 2).map((service, index) => {
const Icon = service.icon;
const href = service.useBasePath ? `${basePath}/${service.id}` : service.fixedPath;
const accent = serviceAccents[service.id];
return (
<Link
key={service.id}
href={href!}
className="group relative overflow-hidden rounded-2xl bg-white border border-border/60 shadow-md p-6 sm:p-8 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg animate-in fade-in slide-in-from-bottom-8 duration-700"
style={{ animationDelay: `${index * 100}ms` }}
className="group relative overflow-hidden rounded-2xl bg-card border border-border shadow-sm hover:shadow-md transition-all duration-300 animate-in fade-in slide-in-from-bottom-6 duration-600"
style={{ animationDelay: `${index * 80}ms` }}
>
{/* Badge */}
{service.badge && (
<div className="absolute top-4 right-4 sm:top-6 sm:right-6">
<span className="inline-flex items-center rounded-full bg-primary px-3 py-1 text-xs font-bold text-white uppercase tracking-wide shadow-sm">
{service.badge}
</span>
{/* Top accent stripe */}
<div className={`h-1 w-full bg-gradient-to-r ${accent.stripe}`} />
<div className="p-6">
{/* Badge */}
{service.badge && (
<div className="absolute top-5 right-5">
<span
className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold ${accent.badge}`}
>
{service.badge}
</span>
</div>
)}
{/* Icon + subtitle */}
<div
className={`w-11 h-11 rounded-xl ${accent.icon} flex items-center justify-center mb-4`}
>
<Icon className="h-5.5 w-5.5" />
</div>
)}
{/* Icon */}
<div className="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl bg-primary/10 flex items-center justify-center mb-5 transition-transform duration-300 group-hover:scale-105">
<Icon className="h-7 w-7 sm:h-8 sm:w-8 text-primary" />
</div>
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-1">
{service.subtitle}
</p>
{/* Subtitle */}
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1 block">
{service.subtitle}
</span>
<h3 className="text-xl font-bold text-foreground mb-2">{service.title}</h3>
{/* Title */}
<h3 className="text-xl sm:text-2xl font-bold text-foreground mb-3">
{service.title}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed mb-4">
{service.description}
</p>
{/* Description */}
<p className="text-muted-foreground leading-relaxed mb-5">
{service.description}
</p>
{/* Price + Features row */}
<div className="flex items-end justify-between gap-4">
<div className="flex flex-wrap gap-1.5">
{service.features.map(feature => (
<span
key={feature}
className="inline-flex items-center gap-1 rounded-md bg-muted/60 px-2 py-1 text-[11px] font-medium text-foreground/80"
>
<Check className="h-3 w-3 text-primary/70" />
{feature}
</span>
))}
</div>
{/* Features */}
<div className="flex flex-wrap gap-2 mb-5">
{service.features.map(feature => (
<span
key={feature}
className="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1.5 text-xs font-medium text-foreground"
>
<Check className="h-3 w-3 text-primary" />
{feature}
</span>
))}
</div>
{service.price && (
<div className="text-right flex-shrink-0">
<span className="text-lg font-bold text-foreground">{service.price}</span>
<span className="text-xs text-muted-foreground">{service.priceUnit}</span>
</div>
)}
</div>
{/* CTA */}
<div className="flex items-center gap-2 text-primary font-semibold group-hover:gap-3 transition-all duration-300">
<span>View Plans</span>
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
{/* Hover CTA */}
<div className="flex items-center gap-1.5 text-primary font-medium text-sm mt-4 group-hover:gap-2.5 transition-all duration-300">
<span>View Plans</span>
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
</div>
</div>
</Link>
);
})}
</div>
{/* Secondary services - Bottom row */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{/* Secondary Services — VPN, Business, Onsite */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{services.slice(2).map((service, index) => {
const Icon = service.icon;
const href = service.useBasePath ? `${basePath}/${service.id}` : service.fixedPath;
const accent = serviceAccents[service.id];
return (
<Link
key={service.id}
href={href!}
className="group relative overflow-hidden rounded-2xl bg-white border border-border/60 shadow-sm p-5 sm:p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-md animate-in fade-in slide-in-from-bottom-8 duration-700"
style={{ animationDelay: `${(index + 2) * 100}ms` }}
className="group relative overflow-hidden rounded-xl bg-card border border-border shadow-sm hover:shadow-md transition-all duration-300 animate-in fade-in slide-in-from-bottom-6 duration-600"
style={{ animationDelay: `${(index + 2) * 80}ms` }}
>
<div className="flex items-start gap-4">
{/* Icon */}
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center flex-shrink-0 transition-transform duration-300 group-hover:scale-105">
<Icon className="h-6 w-6 text-primary" />
</div>
{/* Left accent stripe */}
<div
className={`absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b ${accent.stripe}`}
/>
<div className="flex-1 min-w-0">
{/* Subtitle */}
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-0.5 block">
{service.subtitle}
</span>
<div className="p-5 pl-6">
<div className="flex items-start gap-3.5">
<div
className={`w-10 h-10 rounded-lg ${accent.icon} flex items-center justify-center flex-shrink-0`}
>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-0.5">
{service.subtitle}
</p>
<h3 className="text-base font-bold text-foreground mb-1.5">
{service.title}
</h3>
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-2 mb-3">
{service.description}
</p>
{/* Title */}
<h3 className="text-lg font-bold text-foreground mb-2">{service.title}</h3>
{/* Description */}
<p className="text-sm text-muted-foreground leading-relaxed mb-3 line-clamp-2">
{service.description}
</p>
{/* Features as pills */}
<div className="flex flex-wrap gap-1.5">
{service.features.slice(0, 2).map(feature => (
<span
key={feature}
className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-foreground"
>
{feature}
</span>
))}
{/* Feature pills */}
<div className="flex flex-wrap gap-1">
{service.features.map(feature => (
<span
key={feature}
className="inline-flex items-center rounded bg-muted/50 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground"
>
{feature}
</span>
))}
</div>
</div>
</div>
</div>
{/* Hover arrow indicator */}
<div className="absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ArrowRight className="h-4 w-4 text-primary" />
{/* Hover arrow */}
<div className="absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<ArrowRight className="h-4 w-4 text-primary" />
</div>
</div>
</Link>
);
})}
</div>
</div>
{/* Gradient fade to CTA section */}
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-white pointer-events-none" />
</section>
{/* CTA Section */}
{showCta && (
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-12 sm:py-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-2xl sm:text-3xl font-bold text-foreground mb-4">
<section className="py-14">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-xl sm:text-2xl font-bold text-foreground mb-3">
Need help choosing?
</h2>
<p className="text-muted-foreground mb-8 max-w-lg mx-auto leading-relaxed">
Our bilingual team is ready to help you find the perfect solution for your needs. Get
personalized recommendations in English.
<p className="text-sm text-muted-foreground mb-6 max-w-md mx-auto leading-relaxed">
Our bilingual team can help you find the right solution. Get personalized
recommendations in English.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<Link
href="/contact"
className="inline-flex items-center gap-2 rounded-full bg-primary px-6 py-3 font-semibold text-white hover:bg-primary/90 transition-colors shadow-md"
className="inline-flex items-center gap-2 rounded-full bg-primary px-5 py-2.5 text-sm font-semibold text-white hover:bg-primary/90 transition-colors shadow-sm"
>
Get in Touch
<ArrowRight className="h-4 w-4" />
<ArrowRight className="h-3.5 w-3.5" />
</Link>
<a
href="tel:0120660470"

View File

@ -1,6 +1,6 @@
"use client";
import { Home, Building2, Zap } from "lucide-react";
import { Home, Building2, Check } from "lucide-react";
import { Button } from "@/components/atoms/button";
import { CardBadge } from "@/features/services/components/base/CardBadge";
import { cn } from "@/shared/utils";
@ -35,16 +35,22 @@ interface InternetOfferingCardProps {
const tierStyles = {
Silver: {
card: "border-muted-foreground/20 bg-card",
card: "border-border/60 bg-card",
accent: "text-muted-foreground",
stripe: "bg-gradient-to-r from-slate-400 to-slate-500",
badge: "bg-slate-100 text-slate-600",
},
Gold: {
card: "border-warning/30 bg-warning-soft/20",
card: "border-warning/25 bg-warning-soft/10",
accent: "text-warning",
stripe: "bg-gradient-to-r from-amber-400 to-yellow-500",
badge: "bg-amber-50 text-amber-700",
},
Platinum: {
card: "border-primary/30 bg-info-soft/20",
card: "border-primary/25 bg-info-soft/10",
accent: "text-primary",
stripe: "bg-gradient-to-r from-sky-500 to-blue-600",
badge: "bg-sky-50 text-sky-700",
},
} as const;
@ -71,38 +77,50 @@ export function InternetOfferingCard({
return (
<div
className={cn(
"rounded-xl border bg-card shadow-[var(--cp-shadow-1)] overflow-hidden",
isPremium ? "border-primary/30" : "border-border"
"rounded-xl border bg-card overflow-hidden",
isPremium ? "border-primary/25 shadow-sm" : "border-border/60"
)}
>
{/* Header - Always visible */}
{/* Top accent stripe */}
<div
className={cn(
"h-0.5 w-full",
isPremium
? "bg-gradient-to-r from-sky-500 to-blue-600"
: "bg-gradient-to-r from-sky-400 to-blue-500"
)}
/>
{/* Header */}
<div className="w-full p-4 flex items-start justify-between gap-3 text-left">
<div className="flex items-start gap-3">
<div
className={cn(
"flex h-10 w-10 items-center justify-center rounded-lg border flex-shrink-0",
"flex h-9 w-9 items-center justify-center rounded-lg flex-shrink-0",
iconType === "home"
? "bg-info-soft/50 text-info border-info/20"
: "bg-success-soft/50 text-success border-success/20"
? "bg-sky-500/10 text-sky-600"
: "bg-emerald-500/10 text-emerald-600"
)}
>
<Icon className="h-5 w-5" />
<Icon className="h-4.5 w-4.5" />
</div>
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-base font-bold text-foreground">{title}</h3>
<div className="space-y-0.5">
<div className="flex flex-wrap items-center gap-1.5">
<h3 className="text-sm font-bold text-foreground">{title}</h3>
<CardBadge text={speedBadge} variant={isPremium ? "new" : "default"} size="sm" />
{isPremium && <span className="text-xs text-muted-foreground">(select areas)</span>}
{isPremium && (
<span className="text-[10px] text-muted-foreground">(select areas)</span>
)}
</div>
<p className="text-sm text-muted-foreground">{description}</p>
<div className="flex items-baseline gap-1 pt-0.5">
<span className="text-xs text-muted-foreground">From</span>
<span className="text-lg font-bold text-foreground">
<p className="text-xs text-muted-foreground">{description}</p>
<div className="flex items-baseline gap-0.5 pt-0.5">
<span className="text-[10px] text-muted-foreground">From</span>
<span className="text-base font-bold text-foreground">
¥{startingPrice.toLocaleString()}
</span>
<span className="text-sm text-muted-foreground">/mo</span>
<span className="text-xs text-muted-foreground ml-1.5">
<span className="text-xs text-muted-foreground">/mo</span>
<span className="text-[10px] text-muted-foreground ml-1">
+ ¥{setupFee.toLocaleString()} setup
</span>
</div>
@ -110,91 +128,95 @@ export function InternetOfferingCard({
</div>
</div>
{/* Tiers - Always expanded */}
<div className="border-t border-border px-4 py-4 bg-muted/10">
{/* Tiers */}
<div className="border-t border-border/60 px-4 py-4 bg-muted/5">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{tiers.map(tier => (
<div
key={tier.tier}
className={cn(
"rounded-lg border p-3 transition-all flex flex-col",
tierStyles[tier.tier].card,
tier.recommended && "ring-1 ring-warning/30"
)}
>
{/* Header */}
<div className="flex items-center gap-2 mb-2">
<span className={cn("font-semibold text-sm", tierStyles[tier.tier].accent)}>
{tier.tier}
</span>
{tier.recommended && (
<CardBadge text="Recommended" variant="recommended" size="xs" />
{tiers.map(tier => {
const style = tierStyles[tier.tier];
return (
<div
key={tier.tier}
className={cn(
"rounded-lg border p-3 transition-all flex flex-col relative overflow-hidden",
style.card,
tier.recommended && "ring-1 ring-warning/25"
)}
</div>
>
{/* Tier accent bar */}
<div className={cn("absolute top-0 left-0 right-0 h-px", style.stripe)} />
{/* Price */}
{!previewMode && (
<div className="mb-2">
<div className="flex items-baseline gap-0.5 flex-wrap">
<span className="text-xl font-bold text-foreground">
¥{tier.monthlyPrice.toLocaleString()}
</span>
<span className="text-xs text-muted-foreground">/mo</span>
{tier.pricingNote && (
<span className="text-[10px] text-warning ml-1">{tier.pricingNote}</span>
)}
</div>
</div>
)}
{/* Description */}
<p className="text-xs text-muted-foreground mb-2">{tier.description}</p>
{/* Features */}
<ul className="space-y-1 flex-grow mb-3">
{tier.features.slice(0, 3).map((feature, index) => (
<li key={index} className="flex items-start gap-1.5 text-xs">
<Zap className="h-3 w-3 text-success flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground leading-relaxed">{feature}</span>
</li>
))}
</ul>
{/* Button */}
{previewMode ? (
<div className="mt-auto pt-2 border-t border-border/50">
<p className="text-[10px] text-muted-foreground text-center">
See pricing after verification
</p>
</div>
) : disabled ? (
<div className="mt-auto">
<Button variant="outline" size="sm" className="w-full" disabled>
Unavailable
</Button>
{disabledReason && (
<p className="text-[10px] text-muted-foreground text-center mt-1.5">
{disabledReason}
</p>
{/* Tier header */}
<div className="flex items-center gap-1.5 mb-2">
<span className={cn("font-semibold text-xs", style.accent)}>{tier.tier}</span>
{tier.recommended && (
<CardBadge text="Recommended" variant="recommended" size="xs" />
)}
</div>
) : (
<Button
as="a"
href={resolveTierHref(ctaPath, tier.planSku)}
variant={tier.recommended ? "default" : "outline"}
size="sm"
className="w-full mt-auto"
>
Select
</Button>
)}
</div>
))}
{/* Price */}
{!previewMode && (
<div className="mb-2">
<div className="flex items-baseline gap-0.5 flex-wrap">
<span className="text-lg font-bold text-foreground">
¥{tier.monthlyPrice.toLocaleString()}
</span>
<span className="text-[10px] text-muted-foreground">/mo</span>
{tier.pricingNote && (
<span className="text-[10px] text-warning ml-1">{tier.pricingNote}</span>
)}
</div>
</div>
)}
{/* Description */}
<p className="text-[11px] text-muted-foreground mb-2">{tier.description}</p>
{/* Features */}
<ul className="space-y-1 flex-grow mb-3">
{tier.features.slice(0, 3).map((feature, index) => (
<li key={index} className="flex items-start gap-1.5 text-[11px]">
<Check className="h-3 w-3 text-primary/60 flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground leading-relaxed">{feature}</span>
</li>
))}
</ul>
{/* Button */}
{previewMode ? (
<div className="mt-auto pt-2 border-t border-border/40">
<p className="text-[10px] text-muted-foreground text-center">
See pricing after verification
</p>
</div>
) : disabled ? (
<div className="mt-auto">
<Button variant="outline" size="sm" className="w-full" disabled>
Unavailable
</Button>
{disabledReason && (
<p className="text-[10px] text-muted-foreground text-center mt-1">
{disabledReason}
</p>
)}
</div>
) : (
<Button
as="a"
href={resolveTierHref(ctaPath, tier.planSku)}
variant={tier.recommended ? "default" : "outline"}
size="sm"
className="w-full mt-auto"
>
Select
</Button>
)}
</div>
);
})}
</div>
{/* Footer */}
<p className="text-xs text-muted-foreground text-center mt-3 pt-3 border-t border-border/50">
<p className="text-[11px] text-muted-foreground text-center mt-3 pt-2.5 border-t border-border/40">
+ ¥{setupFee.toLocaleString()} one-time installation (or 12/24-month installment)
</p>
</div>

View File

@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { ChevronDown, ChevronUp, Home, Building2, Info, X, Sparkles } from "lucide-react";
import { ChevronDown, ChevronUp, Home, Building2, Info, X, Sparkles, Check } from "lucide-react";
import { Button } from "@/components/atoms/button";
import { CardBadge } from "@/features/services/components/base/CardBadge";
import { cn } from "@/shared/utils";
@ -38,29 +38,29 @@ interface PublicOfferingCardProps {
const tierStyles = {
Silver: {
card: "border-gray-200 bg-white border-l-4 border-l-gray-400",
accent: "text-gray-600",
card: "border-border/60 bg-card",
accent: "text-muted-foreground",
leftBorder: "border-l-slate-400",
},
Gold: {
card: "border-gray-200 bg-white border-l-4 border-l-amber-500",
card: "border-border/60 bg-card",
accent: "text-amber-600",
leftBorder: "border-l-amber-500",
},
Platinum: {
card: "border-gray-200 bg-white border-l-4 border-l-primary",
card: "border-border/60 bg-card",
accent: "text-primary",
leftBorder: "border-l-primary",
},
} as const;
/**
* Info panel explaining apartment connection types
*/
function ConnectionTypeInfo({ onClose }: { onClose: () => void }) {
return (
<div className="bg-info-soft/50 border border-info/20 rounded-lg p-4 mb-4">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="bg-info-soft/30 border border-info/15 rounded-lg p-3.5 mb-4">
<div className="flex items-start justify-between gap-3 mb-2.5">
<div className="flex items-center gap-2">
<Info className="h-5 w-5 text-info flex-shrink-0" />
<h4 className="font-semibold text-sm text-foreground">
<Info className="h-4 w-4 text-info flex-shrink-0" />
<h4 className="font-semibold text-xs text-foreground">
Why does speed vary by building?
</h4>
</div>
@ -69,15 +69,15 @@ function ConnectionTypeInfo({ onClose }: { onClose: () => void }) {
onClick={onClose}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<X className="h-4 w-4" />
<X className="h-3.5 w-3.5" />
</button>
</div>
<div className="space-y-3 text-xs text-muted-foreground">
<div className="space-y-2.5 text-[11px] text-muted-foreground">
<p>
Apartment buildings in Japan have different fiber infrastructure installed by NTT. Your
available speed depends on what your building supports:
</p>
<div className="grid gap-2">
<div className="grid gap-1.5">
<div className="flex gap-2">
<span className="font-semibold text-foreground whitespace-nowrap">FTTH (1Gbps)</span>
<span>
@ -97,7 +97,7 @@ function ConnectionTypeInfo({ onClose }: { onClose: () => void }) {
</span>
</div>
</div>
<p className="text-foreground font-medium pt-1">
<p className="text-foreground font-medium pt-0.5">
Good news: All types have the same monthly price (¥4,800~). We&apos;ll check what&apos;s
available at your address.
</p>
@ -107,8 +107,8 @@ function ConnectionTypeInfo({ onClose }: { onClose: () => void }) {
}
/**
* Public-facing offering card that shows pricing inline
* No modals - all information is visible or expandable within the card
* Public-facing offering card that shows pricing inline.
* No modals all information is visible or expandable within the card.
*/
export function PublicOfferingCard({
title,
@ -134,49 +134,61 @@ export function PublicOfferingCard({
return (
<div
className={cn(
"rounded-xl border bg-card shadow-[var(--cp-shadow-1)] overflow-hidden transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5",
isExpanded ? "shadow-[var(--cp-shadow-2)] ring-1 ring-primary/20" : "",
isPremium ? "border-primary/30" : "border-border"
"rounded-xl border bg-card overflow-hidden transition-all duration-300",
isExpanded ? "shadow-md ring-1 ring-primary/15" : "hover:shadow-sm",
isPremium ? "border-primary/25" : "border-border/60"
)}
>
{/* Header - Always visible */}
{/* Top accent stripe */}
<div
className={cn(
"h-0.5 w-full",
isPremium
? "bg-gradient-to-r from-sky-500 to-blue-600"
: "bg-gradient-to-r from-sky-400 to-blue-500"
)}
/>
{/* Header */}
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full p-4 flex items-start justify-between gap-3 text-left hover:bg-muted/20 transition-colors"
className="w-full p-4 flex items-start justify-between gap-3 text-left hover:bg-muted/10 transition-colors"
>
<div className="flex items-start gap-3">
<div
className={cn(
"flex h-10 w-10 items-center justify-center rounded-lg border flex-shrink-0",
"flex h-9 w-9 items-center justify-center rounded-lg flex-shrink-0",
iconType === "home"
? "bg-info-soft/50 text-info border-info/20"
: "bg-success-soft/50 text-success border-success/20"
? "bg-sky-500/10 text-sky-600"
: "bg-emerald-500/10 text-emerald-600"
)}
>
<Icon className="h-5 w-5" />
<Icon className="h-4.5 w-4.5" />
</div>
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-base font-bold text-foreground">{title}</h3>
<div className="space-y-0.5">
<div className="flex flex-wrap items-center gap-1.5">
<h3 className="text-sm font-bold text-foreground">{title}</h3>
<CardBadge text={speedBadge} variant={isPremium ? "new" : "default"} size="sm" />
{isPremium && <span className="text-xs text-muted-foreground">(select areas)</span>}
{isPremium && (
<span className="text-[10px] text-muted-foreground">(select areas)</span>
)}
</div>
<p className="text-sm text-muted-foreground">{description}</p>
<div className="flex items-baseline gap-1 pt-0.5">
<span className="text-xs text-muted-foreground">From</span>
<span className="text-lg font-bold text-foreground">
<p className="text-xs text-muted-foreground">{description}</p>
<div className="flex items-baseline gap-0.5 pt-0.5">
<span className="text-[10px] text-muted-foreground">From</span>
<span className="text-base font-bold text-foreground">
¥{startingPrice.toLocaleString()}
{maxPrice && maxPrice > startingPrice && `~${maxPrice.toLocaleString()}`}
</span>
<span className="text-sm text-muted-foreground">/mo</span>
<span className="text-xs text-muted-foreground">/mo</span>
</div>
</div>
</div>
<div className="flex items-center gap-1.5 flex-shrink-0 mt-1">
<span className="text-xs text-muted-foreground hidden sm:inline">
<div className="flex items-center gap-1 flex-shrink-0 mt-1">
<span className="text-[10px] text-muted-foreground hidden sm:inline">
{isExpanded ? "Hide" : "View tiers"}
</span>
{isExpanded ? (
@ -187,104 +199,95 @@ export function PublicOfferingCard({
</div>
</button>
{/* Expanded content - Tier pricing shown inline */}
{/* Expanded: Tier pricing */}
{isExpanded && (
<div className="border-t border-border px-4 py-4 bg-muted/10">
{/* Connection type info button (for Apartment) */}
<div className="border-t border-border/60 px-4 py-4 bg-muted/5">
{showConnectionInfo && !showInfo && (
<button
type="button"
onClick={() => setShowInfo(true)}
className="flex items-center gap-1.5 text-xs text-info hover:text-info/80 transition-colors mb-3"
className="flex items-center gap-1.5 text-[11px] text-info hover:text-info/80 transition-colors mb-3"
>
<Info className="h-4 w-4" />
<Info className="h-3.5 w-3.5" />
<span>Why does speed vary by building?</span>
</button>
)}
{/* Connection type info panel */}
{showConnectionInfo && showInfo && (
<ConnectionTypeInfo onClose={() => setShowInfo(false)} />
)}
{/* Tier cards - 3 columns on desktop */}
{/* Tier cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
{tiers.map(tier => (
<div
key={tier.tier}
className={cn(
"rounded-lg border p-3 transition-all duration-200 flex flex-col relative",
tierStyles[tier.tier].card
)}
>
{/* Popular Badge for Gold */}
{tier.tier === "Gold" && (
<div className="absolute -top-2.5 left-1/2 -translate-x-1/2">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-500 text-white text-[10px] font-semibold shadow-sm">
<Sparkles className="h-2.5 w-2.5" />
Popular
</span>
</div>
)}
{/* Header */}
{tiers.map(tier => {
const style = tierStyles[tier.tier];
return (
<div
className={cn("flex items-center gap-2 mb-2", tier.tier === "Gold" ? "mt-1" : "")}
key={tier.tier}
className={cn(
"rounded-lg border border-l-4 p-3 transition-all duration-200 flex flex-col relative",
style.card,
style.leftBorder
)}
>
<span className={cn("font-semibold text-sm", tierStyles[tier.tier].accent)}>
{tier.tier}
</span>
</div>
{/* Price - Always visible */}
<div className="mb-2">
<div className="flex items-baseline gap-0.5 flex-wrap">
<span className="text-xl font-bold text-foreground">
¥{tier.monthlyPrice.toLocaleString()}
{tier.maxMonthlyPrice &&
tier.maxMonthlyPrice > tier.monthlyPrice &&
`~${tier.maxMonthlyPrice.toLocaleString()}`}
</span>
<span className="text-xs text-muted-foreground">/mo</span>
{tier.pricingNote && (
<span
className={`text-[10px] ml-1 ${tier.tier === "Platinum" ? "text-primary" : "text-amber-600"}`}
>
{tier.pricingNote}
{tier.tier === "Gold" && (
<div className="absolute -top-2.5 left-1/2 -translate-x-1/2">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-500 text-white text-[10px] font-semibold shadow-sm">
<Sparkles className="h-2.5 w-2.5" />
Popular
</span>
</div>
)}
<div
className={cn(
"flex items-center gap-2 mb-2",
tier.tier === "Gold" ? "mt-1" : ""
)}
>
<span className={cn("font-semibold text-xs", style.accent)}>{tier.tier}</span>
</div>
<div className="mb-2">
<div className="flex items-baseline gap-0.5 flex-wrap">
<span className="text-lg font-bold text-foreground">
¥{tier.monthlyPrice.toLocaleString()}
{tier.maxMonthlyPrice &&
tier.maxMonthlyPrice > tier.monthlyPrice &&
`~${tier.maxMonthlyPrice.toLocaleString()}`}
</span>
<span className="text-[10px] text-muted-foreground">/mo</span>
{tier.pricingNote && (
<span
className={cn(
"text-[10px] ml-1",
tier.tier === "Platinum" ? "text-primary" : "text-amber-600"
)}
>
{tier.pricingNote}
</span>
)}
</div>
</div>
<p className="text-[11px] text-muted-foreground mb-2">{tier.description}</p>
<ul className="space-y-1 flex-grow">
{tier.features.slice(0, 3).map((feature, index) => (
<li key={index} className="flex items-start gap-1.5 text-[11px]">
<Check className="h-3 w-3 text-primary/60 flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground leading-relaxed">{feature}</span>
</li>
))}
</ul>
</div>
{/* Description */}
<p className="text-xs text-muted-foreground mb-2">{tier.description}</p>
{/* Features */}
<ul className="space-y-1 flex-grow">
{tier.features.slice(0, 3).map((feature, index) => (
<li key={index} className="flex items-start gap-1.5 text-xs">
<svg
className="h-3 w-3 text-gray-500 flex-shrink-0 mt-0.5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-muted-foreground leading-relaxed">{feature}</span>
</li>
))}
</ul>
</div>
))}
);
})}
</div>
{/* Footer with setup fee and CTA */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 pt-3 border-t border-border/50">
<p className="text-xs text-muted-foreground flex-1">
{/* Footer */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 pt-2.5 border-t border-border/40">
<p className="text-[11px] text-muted-foreground flex-1">
<span className="font-semibold text-foreground">
+ ¥{setupFee.toLocaleString()} one-time setup
</span>{" "}

View File

@ -1,101 +1,31 @@
"use client";
import { Signal, FileCheck, Send, CheckCircle } from "lucide-react";
import { HowItWorks, type HowItWorksStep } from "@/features/services/components/base/HowItWorks";
interface StepProps {
number: number;
icon: React.ReactNode;
title: string;
description: string;
}
function Step({ number, icon, title, description }: StepProps) {
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>
);
}
const simSteps: HowItWorksStep[] = [
{
icon: <Signal className="h-6 w-6" />,
title: "Choose Plan",
description: "Select your data and voice options",
},
{
icon: <FileCheck className="h-6 w-6" />,
title: "Create Account",
description: "Sign up with email verification",
},
{
icon: <Send className="h-6 w-6" />,
title: "Place Order",
description: "Configure SIM type and pay",
},
{
icon: <CheckCircle className="h-6 w-6" />,
title: "Get Connected",
description: "Receive SIM and activate",
},
];
export function SimHowItWorksSection() {
const steps = [
{
icon: <Signal className="h-6 w-6" />,
title: "Choose Plan",
description: "Select your data and voice options",
},
{
icon: <FileCheck className="h-6 w-6" />,
title: "Create Account",
description: "Sign up with email verification",
},
{
icon: <Send className="h-6 w-6" />,
title: "Place Order",
description: "Configure SIM type and pay",
},
{
icon: <CheckCircle className="h-6 w-6" />,
title: "Get Connected",
description: "Receive SIM and activate",
},
];
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">
Getting Started
</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) => (
<Step
key={index}
number={index + 1}
icon={step.icon}
title={step.title}
description={step.description}
/>
))}
</div>
</div>
</section>
);
return <HowItWorks steps={simSteps} eyebrow="Getting Started" title="How It Works" />;
}

View File

@ -33,6 +33,7 @@ import {
type HighlightFeature,
} from "@/features/services/components/base/ServiceHighlights";
import { SimHowItWorksSection } from "@/features/services/components/sim/SimHowItWorksSection";
import { cn } from "@/shared/utils";
export type SimPlansTab = "data-voice" | "data-only" | "voice-only";
@ -56,24 +57,30 @@ function CollapsibleSection({
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="border border-border rounded-xl overflow-hidden bg-card">
<div className="border border-border/60 rounded-xl overflow-hidden bg-card">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors"
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/30 transition-colors"
>
<div className="flex items-center gap-3">
<Icon className="w-5 h-5 text-primary" />
<span className="font-medium text-foreground">{title}</span>
<div className="flex items-center gap-2.5">
<Icon className="w-4 h-4 text-primary" />
<span className="text-sm font-medium text-foreground">{title}</span>
</div>
<ChevronDown
className={`w-5 h-5 text-muted-foreground transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
className={cn(
"w-4 h-4 text-muted-foreground transition-transform duration-200",
isOpen && "rotate-180"
)}
/>
</button>
<div
className={`overflow-hidden transition-all duration-300 ${isOpen ? "max-h-[2000px]" : "max-h-0"}`}
className={cn(
"overflow-hidden transition-all duration-300",
isOpen ? "max-h-[2000px]" : "max-h-0"
)}
>
<div className="p-4 pt-0 border-t border-border">{children}</div>
<div className="p-4 pt-0 border-t border-border/60">{children}</div>
</div>
</div>
);
@ -92,53 +99,68 @@ function SimPlanCardCompact({
return (
<div
className={`group relative bg-card rounded-2xl p-5 transition-all duration-200 ${
className={cn(
"group relative bg-card rounded-xl overflow-hidden transition-all duration-200",
isFamily
? "border-2 border-success/50 hover:border-success hover:shadow-[var(--cp-shadow-2)]"
: "border border-border hover:border-primary/50 hover:shadow-[var(--cp-shadow-2)]"
}`}
>
{isFamily && (
<div className="absolute -top-3 left-4 flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-success text-success-foreground text-xs font-medium">
<Users className="w-3.5 h-3.5" />
Family Discount
</div>
? "border border-success/40 hover:border-success hover:shadow-md"
: "border border-border hover:border-primary/40 hover:shadow-md"
)}
>
{/* Top accent stripe */}
<div
className={cn(
"h-0.5 w-full",
isFamily
? "bg-gradient-to-r from-emerald-500 to-teal-500"
: "bg-gradient-to-r from-sky-500 to-blue-500"
)}
/>
<div className="flex items-center justify-between mb-4 mt-1">
<div className="flex items-center gap-2">
<div className="p-4">
{isFamily && (
<div className="flex items-center gap-1.5 mb-3">
<Users className="w-3.5 h-3.5 text-success" />
<span className="text-[10px] font-semibold text-success uppercase tracking-wide">
Family Discount
</span>
</div>
)}
{/* Data size — the hero of each card */}
<div className="flex items-center gap-2 mb-3">
<div
className={`w-10 h-10 rounded-xl flex items-center justify-center ${
isFamily ? "bg-success/10" : "bg-primary/10"
}`}
className={cn(
"w-9 h-9 rounded-lg flex items-center justify-center",
isFamily ? "bg-success/10" : "bg-primary/8"
)}
>
<Signal className={`w-5 h-5 ${isFamily ? "text-success" : "text-primary"}`} />
<Signal className={cn("w-4 h-4", isFamily ? "text-success" : "text-primary")} />
</div>
<span className="text-lg font-bold text-foreground">{plan.simDataSize}</span>
</div>
{/* Pricing */}
<div className="mb-3">
<CardPricing monthlyPrice={displayPrice} size="sm" alignment="left" />
{isFamily && (
<div className="text-[10px] text-success font-medium mt-0.5">Discounted price</div>
)}
</div>
{/* Plan name */}
<p className="text-xs text-muted-foreground mb-4 line-clamp-2">{plan.name}</p>
{/* CTA */}
<Button
className="w-full"
variant="outline"
size="sm"
onClick={() => onSelect(plan.sku)}
rightIcon={<ArrowRight className="w-3.5 h-3.5" />}
>
Select Plan
</Button>
</div>
<div className="mb-4">
<CardPricing monthlyPrice={displayPrice} size="sm" alignment="left" />
{isFamily && (
<div className="text-xs text-success font-medium mt-1">Discounted price applied</div>
)}
</div>
<p className="text-sm text-muted-foreground mb-5 line-clamp-2">{plan.name}</p>
<Button
className={`w-full ${
isFamily
? "group-hover:bg-success group-hover:text-success-foreground"
: "group-hover:bg-primary group-hover:text-primary-foreground"
}`}
variant="outline"
onClick={() => onSelect(plan.sku)}
rightIcon={<ArrowRight className="w-4 h-4" />}
>
Select Plan
</Button>
</div>
);
}
@ -206,29 +228,23 @@ export function SimPlansContent({
if (isLoading) {
return (
<div className="max-w-6xl mx-auto px-4 pb-16 pt-8">
<div className="max-w-5xl mx-auto px-4 pb-16 pt-6">
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
<div className="text-center mb-12 pt-8">
<Skeleton className="h-10 w-80 mx-auto mb-4" />
<Skeleton className="h-6 w-96 max-w-full mx-auto" />
<div className="text-center mb-10 pt-6">
<Skeleton className="h-8 w-72 mx-auto mb-3" />
<Skeleton className="h-5 w-96 max-w-full mx-auto" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="bg-card rounded-2xl border border-border p-5">
<div className="flex items-center justify-between mb-6 mt-1">
<div className="flex items-center gap-2">
<Skeleton className="h-10 w-10 rounded-xl" />
<Skeleton className="h-6 w-16" />
</div>
<div key={i} className="bg-card rounded-xl border border-border p-4">
<div className="flex items-center gap-2 mb-4">
<Skeleton className="h-9 w-9 rounded-lg" />
<Skeleton className="h-5 w-14" />
</div>
<div className="mb-6 space-y-2">
<Skeleton className="h-8 w-32" />
</div>
<div className="space-y-2 mb-6">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<Skeleton className="h-10 w-full rounded-md" />
<Skeleton className="h-7 w-28 mb-3" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4 mb-4" />
<Skeleton className="h-9 w-full rounded-md" />
</div>
))}
</div>
@ -239,9 +255,9 @@ export function SimPlansContent({
if (error) {
const errorMessage = error instanceof Error ? error.message : "An unexpected error occurred";
return (
<div className="max-w-6xl mx-auto px-4 pb-16 pt-8">
<div className="max-w-5xl mx-auto px-4 pb-16 pt-6">
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
<div className="rounded-xl bg-destructive/10 border border-destructive/20 p-8 text-center mt-8">
<div className="rounded-xl bg-destructive/10 border border-destructive/20 p-8 text-center mt-6">
<div className="text-destructive font-medium text-lg mb-2">Failed to load SIM plans</div>
<div className="text-destructive/80 text-sm mb-6">{errorMessage}</div>
<Button as="a" href={servicesBasePath} leftIcon={<ArrowLeft className="w-4 h-4" />}>
@ -280,91 +296,85 @@ export function SimPlansContent({
const { regularPlans, familyPlans } = getCurrentPlans();
return (
<div className="max-w-6xl mx-auto py-8 pb-16 space-y-12">
<div className="max-w-5xl mx-auto py-6 pb-16 space-y-8">
<ServicesBackLink href={servicesBasePath} label="Back to Services" className="mb-0" />
<ServicesHero
title="SIM Cards for Foreigners in Japan"
description="No hanko, no complicated Japanese forms. Get a SIM card with your foreign credit card and English support. eSIM for instant delivery or physical SIM shipped to your door."
eyebrow={
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20">
<Sparkles className="w-4 h-4 text-primary" />
<span className="text-sm font-medium text-primary">Powered by NTT DOCOMO</span>
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/8 border border-primary/15">
<Sparkles className="w-3.5 h-3.5 text-primary" />
<span className="text-xs font-medium text-primary">Powered by NTT DOCOMO</span>
</div>
}
/>
{variant === "account" && hasExistingSim && (
<div className="space-y-4 mb-8">
<AlertBanner variant="success" title="Family Discount Available">
<p className="text-sm">
You already have a SIM subscription. Discounted pricing is automatically shown for
additional lines.
</p>
</AlertBanner>
</div>
<AlertBanner variant="success" title="Family Discount Available">
<p className="text-sm">
You already have a SIM subscription. Discounted pricing is automatically shown for
additional lines.
</p>
</AlertBanner>
)}
<ServiceHighlights features={simFeatures} className="mb-12" />
<ServiceHighlights features={simFeatures} />
<div className="flex justify-center mb-8">
<div className="inline-flex rounded-xl bg-muted p-1 border border-border">
<button
type="button"
onClick={() => onTabChange("data-voice")}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === "data-voice"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
<Phone className="h-4 w-4" />
<span className="hidden sm:inline">Data + Voice</span>
<span className="sm:hidden">All-in</span>
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
{plansByType.DataSmsVoice.length}
</span>
</button>
<button
type="button"
onClick={() => onTabChange("data-only")}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === "data-only"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
<Globe className="h-4 w-4" />
<span className="hidden sm:inline">Data Only</span>
<span className="sm:hidden">Data</span>
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
{plansByType.DataOnly.length}
</span>
</button>
<button
type="button"
onClick={() => onTabChange("voice-only")}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === "voice-only"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
<Check className="h-4 w-4" />
<span className="hidden sm:inline">Voice Only</span>
<span className="sm:hidden">Voice</span>
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
{plansByType.VoiceOnly.length}
</span>
</button>
{/* Tab Switcher */}
<div className="flex justify-center">
<div className="inline-flex rounded-lg bg-muted/60 p-0.5 border border-border/60">
{[
{
key: "data-voice" as const,
icon: Phone,
label: "Data + Voice",
shortLabel: "All-in",
count: plansByType.DataSmsVoice.length,
},
{
key: "data-only" as const,
icon: Globe,
label: "Data Only",
shortLabel: "Data",
count: plansByType.DataOnly.length,
},
{
key: "voice-only" as const,
icon: Check,
label: "Voice Only",
shortLabel: "Voice",
count: plansByType.VoiceOnly.length,
},
].map(tab => (
<button
key={tab.key}
type="button"
onClick={() => onTabChange(tab.key)}
className={cn(
"flex items-center gap-1.5 px-4 py-2 rounded-md text-sm font-medium transition-all",
activeTab === tab.key
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
<tab.icon className="h-3.5 w-3.5" />
<span className="hidden sm:inline">{tab.label}</span>
<span className="sm:hidden">{tab.shortLabel}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/8 text-primary font-semibold">
{tab.count}
</span>
</button>
))}
</div>
</div>
<div id="plans" className="min-h-[300px]">
{/* Plans Grid */}
<div id="plans" className="min-h-[280px]">
{regularPlans.length > 0 || familyPlans.length > 0 ? (
<div className="space-y-8 animate-in fade-in duration-300">
{regularPlans.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{regularPlans.map(plan => (
<SimPlanCardCompact key={plan.id} plan={plan} onSelect={onSelectPlan} />
))}
@ -373,11 +383,11 @@ export function SimPlansContent({
{variant === "account" && hasExistingSim && familyPlans.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-4">
<Users className="h-5 w-5 text-success" />
<h3 className="text-lg font-semibold text-foreground">Family Discount Plans</h3>
<div className="flex items-center gap-2 mb-3">
<Users className="h-4 w-4 text-success" />
<h3 className="text-sm font-semibold text-foreground">Family Discount Plans</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{familyPlans.map(plan => (
<SimPlanCardCompact
key={plan.id}
@ -391,23 +401,22 @@ export function SimPlansContent({
)}
</div>
) : (
<div className="text-center py-16 text-muted-foreground">
<div className="text-center py-12 text-muted-foreground text-sm">
No plans available in this category.
</div>
)}
</div>
{/* Device Compatibility Section - below plans */}
{/* Device Compatibility */}
<DeviceCompatibility />
{/* How It Works Section */}
<div className="mt-12">
<SimHowItWorksSection />
</div>
{/* How It Works */}
<SimHowItWorksSection />
<div className="mt-8 space-y-4">
{/* Collapsible Info Sections */}
<div className="space-y-3">
<CollapsibleSection title="Calling & SMS Rates" icon={Phone}>
<div className="space-y-6 pt-4">
<div className="space-y-5 pt-4">
<div>
<h4 className="text-sm font-medium text-foreground mb-3 flex items-center gap-2">
<span className="w-5 h-3 rounded-sm bg-[#BC002D] relative overflow-hidden flex items-center justify-center">
@ -415,31 +424,33 @@ export function SimPlansContent({
</span>
Domestic (Japan)
</h4>
<div className="grid grid-cols-2 gap-4">
<div className="bg-muted/50 rounded-lg p-4 border border-border">
<div className="text-sm text-muted-foreground mb-1">Voice Calls</div>
<div className="text-xl font-bold text-foreground">
¥10<span className="text-sm font-normal text-muted-foreground">/30 sec</span>
<div className="grid grid-cols-2 gap-3">
<div className="bg-muted/30 rounded-lg p-3 border border-border/60">
<div className="text-xs text-muted-foreground mb-1">Voice Calls</div>
<div className="text-lg font-bold text-foreground">
¥10<span className="text-xs font-normal text-muted-foreground">/30 sec</span>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4 border border-border">
<div className="text-sm text-muted-foreground mb-1">SMS</div>
<div className="text-xl font-bold text-foreground">
¥3<span className="text-sm font-normal text-muted-foreground">/message</span>
<div className="bg-muted/30 rounded-lg p-3 border border-border/60">
<div className="text-xs text-muted-foreground mb-1">SMS</div>
<div className="text-lg font-bold text-foreground">
¥3<span className="text-xs font-normal text-muted-foreground">/message</span>
</div>
</div>
</div>
<p className="text-xs text-muted-foreground mt-2">
<p className="text-[11px] text-muted-foreground mt-2">
Incoming calls and SMS are free. Pay-per-use charges billed 5-6 weeks after usage.
</p>
</div>
<div className="p-4 bg-success/5 border border-success/20 rounded-lg">
<div className="flex items-start gap-3">
<Phone className="w-5 h-5 text-success mt-0.5" />
<div className="p-3 bg-success/5 border border-success/15 rounded-lg">
<div className="flex items-start gap-2.5">
<Phone className="w-4 h-4 text-success mt-0.5" />
<div>
<h4 className="font-medium text-foreground">Unlimited Domestic Calling</h4>
<p className="text-sm text-muted-foreground">
<h4 className="text-sm font-medium text-foreground">
Unlimited Domestic Calling
</h4>
<p className="text-xs text-muted-foreground">
Add unlimited domestic calls for{" "}
<span className="font-semibold text-success">¥3,000/month</span> (available at
checkout)
@ -448,7 +459,7 @@ export function SimPlansContent({
</div>
</div>
<div className="text-sm text-muted-foreground">
<div className="text-xs text-muted-foreground">
<p>
International calling rates vary by country (¥31-148/30 sec). See{" "}
<a
@ -466,60 +477,64 @@ export function SimPlansContent({
</CollapsibleSection>
<CollapsibleSection title="Fees & Discounts" icon={CircleDollarSign}>
<div className="space-y-6 pt-4">
<div className="space-y-5 pt-4">
<div>
<h4 className="text-sm font-medium text-foreground mb-3">One-time Fees</h4>
<div className="space-y-3">
<div className="flex items-center justify-between py-2 border-b border-border">
<span className="text-muted-foreground">Activation Fee</span>
<span className="font-medium text-foreground">¥1,500</span>
<h4 className="text-xs font-medium text-foreground mb-2.5">One-time Fees</h4>
<div className="space-y-2">
<div className="flex items-center justify-between py-1.5 border-b border-border/60">
<span className="text-xs text-muted-foreground">Activation Fee</span>
<span className="text-sm font-medium text-foreground">¥1,500</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-border">
<span className="text-muted-foreground">SIM Replacement (lost/damaged)</span>
<span className="font-medium text-foreground">¥1,500</span>
<div className="flex items-center justify-between py-1.5 border-b border-border/60">
<span className="text-xs text-muted-foreground">
SIM Replacement (lost/damaged)
</span>
<span className="text-sm font-medium text-foreground">¥1,500</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="text-muted-foreground">eSIM Re-download</span>
<span className="font-medium text-foreground">¥1,500</span>
<div className="flex items-center justify-between py-1.5">
<span className="text-xs text-muted-foreground">eSIM Re-download</span>
<span className="text-sm font-medium text-foreground">¥1,500</span>
</div>
</div>
</div>
<div className="p-4 bg-success/5 border border-success/20 rounded-lg">
<h4 className="font-medium text-foreground mb-2">Family Discount</h4>
<p className="text-sm text-muted-foreground">
<div className="p-3 bg-success/5 border border-success/15 rounded-lg">
<h4 className="text-sm font-medium text-foreground mb-1">Family Discount</h4>
<p className="text-xs text-muted-foreground">
<span className="font-semibold text-success">¥300/month off</span> per additional
Voice SIM on your account
</p>
</div>
<p className="text-xs text-muted-foreground">All prices exclude 10% consumption tax.</p>
<p className="text-[11px] text-muted-foreground">
All prices exclude 10% consumption tax.
</p>
</div>
</CollapsibleSection>
<CollapsibleSection title="Important Information & Terms" icon={Info}>
<div className="space-y-6 pt-4 text-sm">
<div className="space-y-5 pt-4 text-sm">
<div>
<h4 className="font-medium text-foreground mb-3 flex items-center gap-2">
<TriangleAlert className="w-4 h-4 text-warning" />
<h4 className="text-xs font-medium text-foreground mb-2.5 flex items-center gap-1.5">
<TriangleAlert className="w-3.5 h-3.5 text-warning" />
Important Notices
</h4>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<ul className="space-y-1.5 text-muted-foreground">
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
ID verification with official documents (name, date of birth, address, photo) is
required during checkout.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
A compatible unlocked device is required. Check compatibility on our website.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
Service may not be available in areas with weak signal. See{" "}
<a
@ -533,14 +548,14 @@ export function SimPlansContent({
.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
SIM is activated as 4G by default. 5G can be requested via your account portal.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
International data roaming is not available. Voice/SMS roaming can be enabled
upon request (¥50,000/month limit).
@ -550,32 +565,32 @@ export function SimPlansContent({
</div>
<div>
<h4 className="font-medium text-foreground mb-3">Contract Terms</h4>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<h4 className="text-xs font-medium text-foreground mb-2.5">Contract Terms</h4>
<ul className="space-y-1.5 text-muted-foreground">
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
<strong className="text-foreground">Minimum contract:</strong> 3 full billing
months. First month (sign-up to end of month) is free and doesn&apos;t count.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
<strong className="text-foreground">Billing cycle:</strong> 1st to end of month.
Regular billing starts the 1st of the following month after sign-up.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
<strong className="text-foreground">Cancellation:</strong> Can be requested
after 3rd month via cancellation form. Monthly fee is incurred in full for
cancellation month.
</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
<strong className="text-foreground">SIM return:</strong> SIM card must be
returned after service termination.
@ -585,18 +600,18 @@ export function SimPlansContent({
</div>
<div>
<h4 className="font-medium text-foreground mb-3">Additional Options</h4>
<ul className="space-y-2 text-muted-foreground">
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<h4 className="text-xs font-medium text-foreground mb-2.5">Additional Options</h4>
<ul className="space-y-1.5 text-muted-foreground">
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>Call waiting and voice mail available as separate paid options.</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>Data plan changes are free and take effect next billing month.</span>
</li>
<li className="flex items-start gap-2">
<span className="text-foreground"></span>
<li className="flex items-start gap-2 text-xs">
<span className="text-foreground/60 mt-0.5">&#8226;</span>
<span>
Voice plan changes require new SIM issuance and standard policies apply.
</span>
@ -604,8 +619,8 @@ export function SimPlansContent({
</ul>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<p className="text-xs text-muted-foreground">
<div className="p-3 bg-muted/30 rounded-lg">
<p className="text-[11px] text-muted-foreground">
Payment is by credit card only. Data service is not suitable for activities
requiring continuous large data transfers. See full Terms of Service for complete
details.
@ -616,11 +631,16 @@ export function SimPlansContent({
</div>
{/* FAQ Section */}
<div className="mt-12 mb-8">
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">
Frequently Asked Questions
</h2>
<div className="space-y-4 max-w-3xl mx-auto">
<div>
<div className="text-center mb-5">
<p className="text-xs font-semibold text-primary uppercase tracking-wider mb-1.5">
Common Questions
</p>
<h2 className="text-xl sm:text-2xl font-bold leading-tight tracking-tight text-foreground">
Frequently Asked Questions
</h2>
</div>
<div className="space-y-2 max-w-3xl mx-auto">
<FaqItem
question="What is the service contract period?"
answer="The minimum service requirement period is activation month + 3 billing months. After this period, the service will switch to a monthly service and you will be able to cancel at the end of each month."
@ -660,7 +680,8 @@ export function SimPlansContent({
</div>
</div>
<div className="mt-8 text-center text-sm text-muted-foreground">
{/* Footer */}
<div className="text-center text-xs text-muted-foreground">
<p>
All prices exclude 10% consumption tax.{" "}
<a href="#" className="text-primary hover:underline">
@ -676,19 +697,24 @@ function FaqItem({ question, answer }: { question: string; answer: React.ReactNo
const [isOpen, setIsOpen] = useState(false);
return (
<div className="border border-border rounded-xl overflow-hidden bg-card">
<div className="border border-border/60 rounded-xl overflow-hidden bg-card">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-start justify-between gap-3 p-4 text-left hover:bg-muted/50 transition-colors"
className="w-full flex items-start justify-between gap-3 p-4 text-left group"
>
<span className="font-medium text-foreground">{question}</span>
<span className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
{question}
</span>
<ChevronDown
className={`w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
className={cn(
"w-4 h-4 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200",
isOpen && "rotate-180"
)}
/>
</button>
{isOpen && (
<div className="px-4 pb-4 text-sm text-muted-foreground leading-relaxed">{answer}</div>
<div className="px-4 pb-4 text-xs text-muted-foreground leading-relaxed">{answer}</div>
)}
</div>
);

View File

@ -18,65 +18,58 @@ const vpnFeatures = [
export function VpnPlanCard({ plan }: VpnPlanCardProps) {
return (
<AnimatedCard className="p-6 border border-border hover:border-primary/40 transition-all duration-300 hover:shadow-lg flex flex-col h-full bg-card">
{/* Header with icon and name */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-3">
<div className="p-2.5 bg-primary/10 rounded-xl">
<Globe className="h-6 w-6 text-primary" />
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-foreground">{plan.name}</h3>
<span className="text-sm text-muted-foreground">International</span>
<AnimatedCard className="overflow-hidden border border-border hover:border-primary/30 transition-all duration-300 hover:shadow-md flex flex-col h-full bg-card">
{/* Top accent stripe */}
<div className="h-0.5 w-full bg-gradient-to-r from-violet-500 to-purple-600" />
<div className="p-5 flex flex-col h-full">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-3">
<div className="p-2 bg-violet-500/10 rounded-lg">
<Globe className="h-5 w-5 text-violet-600" />
</div>
<div>
<h3 className="text-base font-bold text-foreground">{plan.name}</h3>
<span className="text-xs text-muted-foreground">International</span>
</div>
</div>
</div>
<div className="p-1.5 rounded-full border border-primary/30">
<Check className="h-4 w-4 text-primary" />
{/* Pricing */}
<div className="mb-4">
<div className="flex items-baseline gap-0.5">
<span className="text-xs text-primary font-medium">¥</span>
<span className="text-2xl font-bold text-foreground">
{(plan.monthlyPrice ?? 0).toLocaleString()}
</span>
<span className="text-xs text-muted-foreground">/month</span>
</div>
<p className="text-[11px] text-muted-foreground mt-0.5">Router rental included</p>
</div>
</div>
{/* Pricing */}
<div className="mb-4">
<div className="flex items-baseline gap-1">
<span className="text-sm text-primary">¥</span>
<span className="text-3xl font-bold text-foreground">
{(plan.monthlyPrice ?? 0).toLocaleString()}
</span>
<span className="text-sm text-muted-foreground">/month</span>
{/* Features */}
<ul className="space-y-2 mb-5 flex-grow">
{vpnFeatures.map((feature, index) => (
<li key={index} className="flex items-start gap-2 text-xs">
<Check className="h-3.5 w-3.5 text-primary/60 flex-shrink-0 mt-0.5" />
<span className="text-muted-foreground">{feature}</span>
</li>
))}
</ul>
{/* CTA */}
<div className="mt-auto">
<Button
as="a"
href={`/order?type=vpn&planSku=${encodeURIComponent(plan.sku)}`}
className="w-full"
size="sm"
rightIcon={<ArrowRight className="w-3.5 h-3.5" />}
>
Select {plan.name}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-1">Router rental included</p>
</div>
{/* Features list */}
<ul className="space-y-2 mb-6 flex-grow">
{vpnFeatures.map((feature, index) => (
<li key={index} className="flex items-start gap-2 text-sm">
<svg
className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-0.5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="text-muted-foreground">{feature}</span>
</li>
))}
</ul>
{/* Action Button */}
<div className="mt-auto">
<Button
as="a"
href={`/order?type=vpn&planSku=${encodeURIComponent(plan.sku)}`}
className="w-full"
rightIcon={<ArrowRight className="w-4 h-4" />}
>
Select {plan.name}
</Button>
</div>
</AnimatedCard>
);

View File

@ -14,7 +14,6 @@ import { ServiceFAQ, type FAQItem } from "@/features/services/components/base/Se
import { VpnPlanCard } from "./VpnPlanCard";
import { VPN_FEATURES } from "@/features/services/utils";
// Steps for HowItWorks
const vpnSteps: HowItWorksStep[] = [
{
icon: <CreditCard className="h-6 w-6" />,
@ -38,7 +37,6 @@ const vpnSteps: HowItWorksStep[] = [
},
];
// FAQ items for VPN
const vpnFaqItems: FAQItem[] = [
{
question: "Which streaming services can I access?",
@ -86,32 +84,29 @@ export function VpnPlansContent({
if (isLoading) {
return (
<div className="max-w-6xl mx-auto px-4 pb-16 pt-8">
<div className="max-w-5xl mx-auto px-4 pb-16 pt-6">
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
<div className="text-center mb-12 pt-8">
<Skeleton className="h-10 w-80 mx-auto mb-4" />
<Skeleton className="h-6 w-96 max-w-full mx-auto" />
<div className="text-center mb-10 pt-6">
<Skeleton className="h-8 w-72 mx-auto mb-3" />
<Skeleton className="h-5 w-96 max-w-full mx-auto" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 max-w-3xl mx-auto">
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="bg-card rounded-2xl border border-border p-6">
<div className="flex items-start gap-4 mb-5">
<Skeleton className="h-14 w-14 rounded-xl" />
<div key={i} className="bg-card rounded-xl border border-border p-5">
<div className="flex items-start gap-3 mb-4">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="flex-1">
<Skeleton className="h-6 w-32 mb-2" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-5 w-28 mb-1.5" />
<Skeleton className="h-3.5 w-20" />
</div>
</div>
<div className="mb-5 space-y-2">
<Skeleton className="h-8 w-36" />
<Skeleton className="h-4 w-28" />
</div>
<div className="space-y-2 mb-6">
<Skeleton className="h-7 w-32 mb-4" />
<div className="space-y-2 mb-5">
{Array.from({ length: 4 }).map((_, j) => (
<Skeleton key={j} className="h-4 w-full" />
<Skeleton key={j} className="h-3.5 w-full" />
))}
</div>
<Skeleton className="h-10 w-full rounded-md" />
<Skeleton className="h-9 w-full rounded-md" />
</div>
))}
</div>
@ -122,9 +117,9 @@ export function VpnPlansContent({
if (error) {
const errorMessage = error instanceof Error ? error.message : "An unexpected error occurred";
return (
<div className="max-w-6xl mx-auto px-4 pb-16 pt-8">
<div className="max-w-5xl mx-auto px-4 pb-16 pt-6">
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
<div className="rounded-xl bg-destructive/10 border border-destructive/20 p-8 text-center mt-8">
<div className="rounded-xl bg-destructive/10 border border-destructive/20 p-8 text-center mt-6">
<div className="text-destructive font-medium text-lg mb-2">Failed to load VPN plans</div>
<div className="text-destructive/80 text-sm mb-6">{errorMessage}</div>
<Button as="a" href={servicesBasePath} leftIcon={<ArrowLeft className="w-4 h-4" />}>
@ -139,41 +134,40 @@ export function VpnPlansContent({
<div className="space-y-8 pb-16">
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
{/* Hero Section */}
<div className="text-center py-6">
{/* Hero */}
<div className="text-center py-4">
<div
className="animate-in fade-in slide-in-from-bottom-4 duration-500"
style={{ animationDelay: "0ms" }}
>
<span className="inline-flex items-center gap-2 rounded-full bg-purple-500/10 border border-purple-500/20 px-4 py-1.5 text-sm text-purple-600 dark:text-purple-400 font-medium mb-4">
<ShieldCheck className="h-4 w-4" />
<span className="inline-flex items-center gap-1.5 rounded-full bg-violet-500/10 border border-violet-500/15 px-3 py-1 text-xs text-violet-600 dark:text-violet-400 font-medium mb-3">
<ShieldCheck className="h-3.5 w-3.5" />
VPN Router Service
</span>
</div>
<h1
className="text-2xl md:text-4xl font-display font-bold leading-tight text-foreground animate-in fade-in slide-in-from-bottom-6 duration-700"
className="text-2xl md:text-3xl font-bold leading-tight tracking-tight text-foreground animate-in fade-in slide-in-from-bottom-6 duration-700"
style={{ animationDelay: "100ms" }}
>
Stream Content from Abroad
</h1>
<p
className="text-lg text-muted-foreground mt-3 max-w-xl mx-auto leading-relaxed animate-in fade-in slide-in-from-bottom-8 duration-700"
className="text-sm text-muted-foreground mt-2 max-w-lg mx-auto leading-relaxed animate-in fade-in slide-in-from-bottom-8 duration-700"
style={{ animationDelay: "200ms" }}
>
Access US and UK streaming services using a pre-configured VPN router. No technical setup
required.
</p>
{/* Order info banner - public variant only */}
{variant === "public" && (
<div
className="inline-flex mt-6 animate-in fade-in slide-in-from-bottom-8 duration-700"
className="inline-flex mt-4 animate-in fade-in slide-in-from-bottom-8 duration-700"
style={{ animationDelay: "300ms" }}
>
<div className="bg-success-soft border border-success/25 rounded-xl px-4 py-3">
<div className="flex items-center gap-2 justify-center">
<Zap className="h-4 w-4 text-success flex-shrink-0" />
<p className="text-sm text-foreground">
<div className="bg-success-soft border border-success/20 rounded-lg px-3 py-2">
<div className="flex items-center gap-1.5 justify-center">
<Zap className="h-3.5 w-3.5 text-success flex-shrink-0" />
<p className="text-xs text-foreground">
<span className="font-medium">Order today</span>
<span className="text-muted-foreground">
{" "}
@ -186,47 +180,45 @@ export function VpnPlansContent({
)}
</div>
{/* Service Highlights */}
<section>
<ServiceHighlights features={VPN_FEATURES} />
</section>
{/* Highlights */}
<ServiceHighlights features={VPN_FEATURES} />
{/* Plans Section */}
{/* Plans */}
{plans.length > 0 ? (
<section
id="plans"
className="animate-in fade-in slide-in-from-bottom-8 duration-700"
style={{ animationDelay: "500ms" }}
>
<div className="text-center mb-6">
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-2">
<div className="text-center mb-5">
<p className="text-xs font-semibold text-primary uppercase tracking-wider mb-1.5">
Choose Your Region
</p>
<h2 className="text-2xl sm:text-3xl font-display font-semibold leading-tight tracking-tight text-foreground">
<h2 className="text-xl sm:text-2xl font-bold leading-tight tracking-tight text-foreground">
Available Plans
</h2>
<p className="text-sm text-muted-foreground mt-2">
<p className="text-xs text-muted-foreground mt-1.5">
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">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 max-w-3xl mx-auto">
{plans.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">
<AlertBanner variant="info" className="mt-5 max-w-3xl mx-auto" title="Activation Fee">
A one-time activation fee of ¥3,000 applies per router rental. Tax (10%) not included.
</AlertBanner>
)}
</section>
) : (
<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">
<div className="text-center py-10">
<ShieldCheck className="h-10 w-10 text-muted-foreground mx-auto mb-3" />
<h3 className="text-base font-medium text-foreground mb-1.5">No VPN Plans Available</h3>
<p className="text-sm text-muted-foreground mb-5">
We couldn&apos;t find any VPN plans available at this time.
</p>
<ServicesBackLink
@ -241,7 +233,7 @@ export function VpnPlansContent({
{/* How It Works */}
<HowItWorks steps={vpnSteps} eyebrow="Simple Setup" title="How It Works" />
{/* CTA Section - public variant only */}
{/* CTA — public only */}
{variant === "public" && (
<ServiceCTA
eyebrow="Get started today"
@ -258,7 +250,7 @@ export function VpnPlansContent({
/>
)}
{/* FAQ Section */}
{/* FAQ */}
<ServiceFAQ
items={vpnFaqItems}
eyebrow="Common Questions"
@ -275,8 +267,8 @@ export function VpnPlansContent({
</p>
</AlertBanner>
{/* Footer Note */}
<div className="text-center text-sm text-muted-foreground">
{/* Footer */}
<div className="text-center text-xs text-muted-foreground">
<p>All prices exclude 10% consumption tax.</p>
</div>
</div>

View File

@ -122,6 +122,24 @@ export function PublicSupportView() {
</p>
</div>
{/* Existing Customer Banner */}
<div className="mb-12 max-w-2xl mx-auto">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 rounded-2xl border border-primary/20 bg-primary/5 px-6 py-5">
<div className="text-center sm:text-left">
<p className="font-semibold text-foreground">Already have an account?</p>
<p className="text-sm text-muted-foreground">
Sign in to view your tickets and get faster support.
</p>
</div>
<Link
href="/auth/login"
className="inline-flex items-center justify-center rounded-full bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground shadow hover:bg-primary/90 transition-colors whitespace-nowrap"
>
Sign in
</Link>
</div>
</div>
{/* Knowledge Base Categories */}
<section className="mb-16" aria-labelledby="kb-heading">
<h2 id="kb-heading" className="text-2xl font-bold text-foreground mb-6 text-center">
@ -262,20 +280,6 @@ export function PublicSupportView() {
<ContactForm />
</div>
</section>
{/* Existing Customer CTA */}
<div className="text-center pt-8 border-t border-border/60">
<p className="text-muted-foreground">
Already have an account?{" "}
<Link
href="/auth/login"
className="font-semibold text-primary hover:text-primary/80 hover:underline transition-colors"
>
Sign in
</Link>{" "}
to access your dashboard and support tickets.
</p>
</div>
</div>
);
}