refactor: update layout and components for improved consistency and accessibility

- Replaced the "About us.png" image with a new "about-us.png" for better naming consistency.
- Updated various sections across the landing page to use a full-bleed layout for a more modern design.
- Refactored ServiceCard component to conditionally render as a link or div based on the presence of an href prop, enhancing flexibility.
- Introduced a new CollapsibleSection component for better organization of content in service-related sections.
- Enhanced styling and structure in multiple components, including ContactSection, CTABanner, and TrustStrip, to improve visual hierarchy and user experience.
This commit is contained in:
barsa 2026-03-05 10:05:30 +09:00
parent 48eb8c8725
commit 7125f79baa
17 changed files with 296 additions and 307 deletions

View File

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 426 KiB

View File

@ -106,7 +106,7 @@ export function OnsiteSupportContent() {
return (
<div className="relative">
{/* Hero Section */}
<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-10 pb-20">
<section className="full-bleed overflow-hidden bg-gradient-to-br from-slate-50 via-white to-sky-50/80 pt-10 pb-20">
{/* Dot grid pattern */}
<div
className="absolute inset-0 pointer-events-none"
@ -195,7 +195,7 @@ export function OnsiteSupportContent() {
</section>
{/* How It Works Section */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-16">
<section className="full-bleed bg-white py-16">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
How It Works
@ -239,7 +239,7 @@ export function OnsiteSupportContent() {
</section>
{/* Pricing Cards Section */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-16">
<section className="full-bleed bg-[#f7f7f7] py-16">
{/* Subtle pattern overlay */}
<div
className="absolute inset-0 pointer-events-none opacity-30"
@ -335,7 +335,7 @@ export function OnsiteSupportContent() {
</section>
{/* FAQ Section */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-16">
<section className="full-bleed bg-white py-16">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
Frequently Asked Questions
@ -387,7 +387,7 @@ export function OnsiteSupportContent() {
</section>
{/* CTA Section */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-16">
<section className="full-bleed bg-[#f7f7f7] py-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2 text-sm text-primary font-medium mb-6">
<Shield className="h-4 w-4" />

View File

@ -35,8 +35,8 @@ export type ServiceCardVariant =
| "bento-lg";
export interface ServiceCardProps {
/** Link destination */
href: string;
/** Link destination (renders as div when omitted) */
href?: string;
/**
* Icon element to display.
* Pass a pre-styled JSX element: `icon={<Wifi className="h-6 w-6" />}`
@ -157,6 +157,28 @@ function renderIcon(icon: ReactNode, className: string): ReactNode {
return icon;
}
/**
* Shared wrapper that renders <Link> when href is provided, <div> otherwise
*/
function CardWrapper({
href,
className,
children,
}: {
href?: string | undefined;
className?: string;
children: ReactNode;
}) {
if (href) {
return (
<Link href={href} className={className}>
{children}
</Link>
);
}
return <div className={className}>{children}</div>;
}
/**
* Default variant - Standard service card
*/
@ -173,7 +195,7 @@ function DefaultCard({
const colors = accentColorStyles[accentColor];
return (
<Link href={href} className={cn("group block", className)}>
<CardWrapper href={href} className={cn("group block", className)}>
<div
className={cn(
"relative h-full flex flex-col rounded-2xl border bg-card p-6",
@ -212,12 +234,14 @@ function DefaultCard({
<p className="text-sm text-muted-foreground leading-relaxed flex-grow">{description}</p>
)}
<div className="mt-4 flex items-center gap-1 text-sm font-medium text-primary">
Learn more
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</div>
{href && (
<div className="mt-4 flex items-center gap-1 text-sm font-medium text-primary">
Learn more
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</div>
)}
</div>
</Link>
</CardWrapper>
);
}
@ -226,7 +250,7 @@ function DefaultCard({
*/
function FeaturedCard({ href, icon, title, description, highlight, className }: ServiceCardProps) {
return (
<Link href={href} className={cn("group block h-full", className)}>
<CardWrapper href={href} className={cn("group block h-full", className)}>
<div
className={cn(
"h-full flex flex-col",
@ -269,19 +293,21 @@ function FeaturedCard({ href, icon, title, description, highlight, className }:
)}
{/* Link indicator */}
<div
className={cn(
"flex items-center gap-1.5 mt-4 pt-4 border-t",
"text-sm font-medium text-primary",
"transition-colors group-hover:text-primary-hover",
"border-primary/10"
)}
>
Learn more
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</div>
{href && (
<div
className={cn(
"flex items-center gap-1.5 mt-4 pt-4 border-t",
"text-sm font-medium text-primary",
"transition-colors group-hover:text-primary-hover",
"border-primary/10"
)}
>
Learn more
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</div>
)}
</div>
</Link>
</CardWrapper>
);
}
@ -290,7 +316,7 @@ function FeaturedCard({ href, icon, title, description, highlight, className }:
*/
function MinimalCard({ href, icon, title, className }: ServiceCardProps) {
return (
<Link href={href} className={cn("group block", className)}>
<CardWrapper href={href} className={cn("group block", className)}>
<div
className={cn(
"flex flex-col items-center text-center",
@ -306,7 +332,7 @@ function MinimalCard({ href, icon, title, className }: ServiceCardProps) {
</div>
<h3 className="text-sm font-semibold text-foreground font-display">{title}</h3>
</div>
</Link>
</CardWrapper>
);
}
@ -317,7 +343,7 @@ function BentoSmallCard({ href, icon, title, accentColor = "blue", className }:
const colors = accentColorStyles[accentColor];
return (
<Link
<CardWrapper
href={href}
className={cn(
"group rounded-xl bg-card/80 backdrop-blur-sm border border-border/50",
@ -333,7 +359,7 @@ function BentoSmallCard({ href, icon, title, accentColor = "blue", className }:
</div>
<span className="font-semibold text-foreground">{title}</span>
</div>
</Link>
</CardWrapper>
);
}
@ -351,7 +377,7 @@ function BentoMediumCard({
const colors = accentColorStyles[accentColor];
return (
<Link
<CardWrapper
href={href}
className={cn(
"group rounded-xl bg-card border",
@ -377,7 +403,7 @@ function BentoMediumCard({
{description && (
<p className="text-sm text-muted-foreground leading-relaxed">{description}</p>
)}
</Link>
</CardWrapper>
);
}
@ -395,7 +421,7 @@ function BentoLargeCard({
const colors = accentColorStyles[accentColor];
return (
<Link
<CardWrapper
href={href}
className={cn(
"group relative overflow-hidden rounded-2xl",
@ -437,19 +463,21 @@ function BentoLargeCard({
<p className="text-muted-foreground leading-relaxed max-w-sm mb-6">{description}</p>
)}
<span
className={cn(
"inline-flex items-center gap-2 font-semibold",
colors.text,
"transition-transform duration-[var(--cp-duration-normal)]",
"group-hover:translate-x-1"
)}
>
Learn more
<ArrowRight className="h-4 w-4" />
</span>
{href && (
<span
className={cn(
"inline-flex items-center gap-2 font-semibold",
colors.text,
"transition-transform duration-[var(--cp-duration-normal)]",
"group-hover:translate-x-1"
)}
>
Learn more
<ArrowRight className="h-4 w-4" />
</span>
)}
</div>
</Link>
</CardWrapper>
);
}

View File

@ -3,10 +3,7 @@ import { Button } from "@/components/atoms/button";
export function CTABanner() {
return (
<section
aria-label="Call to action"
className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-soft"
>
<section aria-label="Call to action" className="full-bleed bg-primary-soft">
<div className="mx-auto max-w-3xl px-6 sm:px-10 lg:px-14 py-14 sm:py-16 text-center">
<h2 className="text-2xl sm:text-3xl font-extrabold text-foreground">
Ready to Get Set Up?

View File

@ -13,7 +13,7 @@ export function ContactSection() {
id="contact"
ref={ref}
className={cn(
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/30 py-14 sm:py-16 transition-all duration-700",
"full-bleed bg-surface-sunken/30 py-14 sm:py-16 transition-all duration-700",
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
)}
>

View File

@ -16,7 +16,7 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
<section
ref={heroRef}
className={cn(
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-16 sm:py-20 lg:py-24 overflow-hidden transition-all duration-700",
"full-bleed py-16 sm:py-20 lg:py-24 overflow-hidden transition-all duration-700",
heroInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
)}
>

View File

@ -299,7 +299,7 @@ export function ServicesCarousel() {
<section
ref={sectionRef}
className={cn(
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/30 py-16 sm:py-20 transition-all duration-700",
"full-bleed bg-surface-sunken/30 py-16 sm:py-20 transition-all duration-700",
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
)}
>

View File

@ -73,7 +73,7 @@ export function TrustStrip() {
ref={ref}
aria-label="Company statistics"
className={cn(
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-10 sm:py-12 overflow-hidden transition-all duration-700",
"full-bleed py-10 sm:py-12 overflow-hidden transition-all duration-700",
inView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
)}
>

View File

@ -19,7 +19,7 @@ export function WhyUsSection() {
<section
ref={ref}
className={cn(
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-background transition-all duration-700",
"full-bleed bg-background transition-all duration-700",
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
)}
>

View File

@ -4,7 +4,7 @@ export function PublicLandingLoadingView() {
return (
<div className="space-y-0 pb-8 pt-0">
{/* Hero Section Skeleton */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken py-16 sm:py-20 lg:py-24">
<section className="full-bleed bg-surface-sunken py-16 sm:py-20 lg:py-24">
<div className="mx-auto max-w-3xl px-6 sm:px-10 lg:px-14 text-center space-y-6">
<div className="space-y-3 mx-auto max-w-lg">
<Skeleton className="h-10 w-72 mx-auto rounded-md" />
@ -22,7 +22,7 @@ export function PublicLandingLoadingView() {
</section>
{/* TrustStrip Skeleton */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/50 border-y border-border/40">
<section className="full-bleed bg-surface-sunken/50 border-y border-border/40">
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-14 py-5">
<div className="grid grid-cols-2 gap-4 sm:flex sm:justify-between">
{Array.from({ length: 4 }).map((_, idx) => (
@ -72,7 +72,7 @@ export function PublicLandingLoadingView() {
</section>
{/* CTABanner Skeleton */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-soft">
<section className="full-bleed bg-primary-soft">
<div className="mx-auto max-w-3xl px-6 sm:px-10 py-14 sm:py-16 text-center space-y-4">
<Skeleton className="h-8 w-64 mx-auto rounded-md" />
<Skeleton className="h-4 w-80 mx-auto rounded-md" />

View File

@ -1,4 +1,5 @@
import Image from "next/image";
import { ServiceCard } from "@/components/molecules";
import type { LucideIcon } from "lucide-react";
import {
Wifi,
@ -21,34 +22,39 @@ import {
/* ─── Data ─── */
const services: { title: string; description: string; icon: LucideIcon }[] = [
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,
icon: <Wifi />,
accentColor: "blue" as const,
},
{
title: "Phone Plans",
description:
"SIM cards on Japan's best network. Foreign credit cards accepted, no hanko required.",
icon: Smartphone,
icon: <Smartphone />,
accentColor: "green" as const,
},
{
title: "Business Solutions",
description:
"Enterprise IT for international companies. Dedicated internet, office networks, and data centers.",
icon: Building2,
icon: <Building2 />,
accentColor: "purple" as const,
},
{
title: "VPN",
description: "Stream your favorite shows from home. Pre-configured router for US/UK content.",
icon: Lock,
icon: <Lock />,
accentColor: "orange" as const,
},
{
title: "Onsite Support",
description: "English-speaking technicians at your door for setup and troubleshooting.",
icon: Wrench,
icon: <Wrench />,
accentColor: "cyan" as const,
},
];
@ -111,10 +117,6 @@ const businessHours = [
{ team: "Onsite Tech Support Team", hours: "Mon \u2013 Sat 10:00AM \u2013 9:00PM" },
];
/* ─── 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;
@ -123,7 +125,7 @@ const HERO_GLOW_STYLE = {
function HeroSection() {
return (
<section className={`${SECTION_BASE} overflow-hidden bg-surface-sunken`}>
<section className="full-bleed 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}
@ -152,7 +154,7 @@ function HeroSection() {
</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"
src="/assets/images/about-us.png"
alt="Assist Solutions team in Tokyo"
fill
priority
@ -168,7 +170,7 @@ function HeroSection() {
function ServicesSection() {
return (
<section className={`${SECTION_BASE} bg-background py-16 sm:py-20`}>
<section className="full-bleed 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>
@ -179,23 +181,16 @@ function ServicesSection() {
</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>
);
})}
{services.map(service => (
<ServiceCard
key={service.title}
variant="bento-md"
icon={service.icon}
title={service.title}
description={service.description}
accentColor={service.accentColor}
/>
))}
</div>
</div>
</section>
@ -204,7 +199,7 @@ function ServicesSection() {
function ValuesSection() {
return (
<section className={`${SECTION_BASE} bg-surface-sunken py-16 sm:py-20`}>
<section className="full-bleed 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>
@ -242,7 +237,7 @@ function ValuesSection() {
function CorporateSection() {
return (
<section className={`${SECTION_BASE} bg-background py-16 sm:py-20`}>
<section className="full-bleed 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>

View File

@ -0,0 +1,50 @@
"use client";
import { useState, type ElementType, type ReactNode } from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "@/shared/utils";
interface CollapsibleSectionProps {
title: string;
icon: ElementType;
defaultOpen?: boolean;
children: ReactNode;
}
export function CollapsibleSection({
title,
icon: Icon,
defaultOpen = false,
children,
}: CollapsibleSectionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<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/30 transition-colors"
>
<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={cn(
"w-4 h-4 text-muted-foreground transition-transform duration-200",
isOpen && "rotate-180"
)}
/>
</button>
<div
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/60">{children}</div>
</div>
</div>
);
}

View File

@ -24,6 +24,11 @@ export interface ServiceCTAProps {
className?: string | undefined;
}
const dotPatternStyle = {
backgroundImage: `radial-gradient(circle at center, color-mix(in oklch, var(--primary) 12%, transparent) 0.6px, transparent 0.6px)`,
backgroundSize: "20px 20px",
} as const;
/**
* ServiceCTA - Call-to-action section with decorative background.
*/
@ -42,13 +47,7 @@ export function ServiceCTA({
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-primary/8" />
{/* Refined dot pattern */}
<div
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",
}}
/>
<div className="absolute inset-0 pointer-events-none opacity-30" style={dotPatternStyle} />
{/* Content */}
<div className="relative">

View File

@ -1,12 +1,12 @@
"use client";
import { useState } from "react";
import { useState, type ReactNode } from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "@/shared/utils/cn";
export interface FAQItem {
question: string;
answer: string;
answer: ReactNode;
}
export interface ServiceFAQProps {
@ -24,7 +24,7 @@ function FAQItemComponent({
onToggle,
}: {
question: string;
answer: string;
answer: ReactNode;
isOpen: boolean;
onToggle: () => void;
}) {
@ -53,7 +53,7 @@ function FAQItemComponent({
)}
>
<div className="pb-4 pr-8">
<p className="text-sm text-muted-foreground leading-relaxed">{answer}</p>
<div className="text-sm text-muted-foreground leading-relaxed">{answer}</div>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
"use client";
import { useMemo, useState, type ElementType, type ReactNode } from "react";
import { useMemo } from "react";
import {
Smartphone,
Check,
@ -10,7 +10,6 @@ import {
Signal,
Sparkles,
CreditCard,
ChevronDown,
Info,
CircleDollarSign,
TriangleAlert,
@ -27,6 +26,8 @@ import { ServicesBackLink } from "@/features/services/components/base/ServicesBa
import { ServicesHero } from "@/features/services/components/base/ServicesHero";
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
import { CardPricing } from "@/features/services/components/base/CardPricing";
import { CollapsibleSection } from "@/features/services/components/base/CollapsibleSection";
import { ServiceFAQ, type FAQItem } from "@/features/services/components/base/ServiceFAQ";
import { DeviceCompatibility } from "./DeviceCompatibility";
import {
ServiceHighlights,
@ -43,48 +44,110 @@ interface PlansByType {
VoiceOnly: SimCatalogProduct[];
}
function CollapsibleSection({
title,
icon: Icon,
defaultOpen = false,
children,
}: {
title: string;
icon: ElementType;
defaultOpen?: boolean;
children: ReactNode;
}) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const simFeatures: HighlightFeature[] = [
{
icon: <CreditCard className="h-6 w-6" />,
title: "Foreign Cards Accepted",
description: "Use your overseas credit card. No Japanese bank account needed",
highlight: "Easy payment",
},
{
icon: <Signal className="h-6 w-6" />,
title: "Japan's Best Network",
description: "NTT Docomo coverage reaches 99.9% of Japan's population",
highlight: "Nationwide",
},
{
icon: <CircleDollarSign className="h-6 w-6" />,
title: "First Month Free",
description: "Try risk-free. Your first month's basic fee is waived",
highlight: "Great value",
},
{
icon: <Calendar className="h-6 w-6" />,
title: "Flexible Terms",
description: "No multi-year contracts. Stay as long as you need",
highlight: "No lock-in",
},
{
icon: <ArrowRightLeft className="h-6 w-6" />,
title: "Keep Your Number",
description: "Switching from another carrier? Bring your Japanese number with you",
highlight: "Easy transfer",
},
{
icon: <Smartphone className="h-6 w-6" />,
title: "Free Plan Changes",
description: "Switch data plans anytime for the next billing cycle",
highlight: "Flexibility",
},
];
return (
<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/30 transition-colors"
>
<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={cn(
"w-4 h-4 text-muted-foreground transition-transform duration-200",
isOpen && "rotate-180"
)}
/>
</button>
<div
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/60">{children}</div>
</div>
</div>
);
}
const SIM_TABS = [
{
key: "data-voice" as const,
icon: Phone,
label: "Data + Voice",
shortLabel: "All-in",
planTypeKey: "DataSmsVoice" as const,
},
{
key: "data-only" as const,
icon: Globe,
label: "Data Only",
shortLabel: "Data",
planTypeKey: "DataOnly" as const,
},
{
key: "voice-only" as const,
icon: Check,
label: "Voice Only",
shortLabel: "Voice",
planTypeKey: "VoiceOnly" as const,
},
];
const SIM_FAQ_ITEMS: 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.",
},
{
question: "I've changed my phone and the SIM card is not working on the new device.",
answer:
"Whenever the SIM card is used with a new device, the APN profile would need to be installed on said device. Please refer to the APN Setup Guide in the Documents section to check how the profile can be installed.",
},
{
question: "Are international calling features available?",
answer: (
<>
Enter &quot;+&quot; or &quot;010&quot;, &quot;recipient&apos;s country code&quot;, and
&quot;recipient&apos;s phone number (regular phone number/mobile phone number)&quot; Make
a call.
<br />
<br />
If the recipient&apos;s phone number begins with a 0, enter it without the first 0 (except
in some countries and regions).
<br />
International calling rate is on the following Docomo&apos;s website:{" "}
<a
href="https://www.docomo.ne.jp/english/service/world/roaming/charges/kaigai/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Docomo International Calling Rates
</a>
</>
),
},
{
question: "How do I cancel the service?",
answer:
"To cancel, please log into the SonixNet SIM Management Website and send us a cancellation request before the 25th to cancel your account at the end of the month. For example, cancellation requests will need to be sent in by May 25th, in order to cancel at the end of May.",
},
];
function SimPlanCardCompact({
plan,
@ -187,45 +250,6 @@ export function SimPlansContent({
const simPlans: SimCatalogProduct[] = useMemo(() => plans ?? [], [plans]);
const hasExistingSim = useMemo(() => simPlans.some(p => p.simHasFamilyDiscount), [simPlans]);
const simFeatures: HighlightFeature[] = [
{
icon: <CreditCard className="h-6 w-6" />,
title: "Foreign Cards Accepted",
description: "Use your overseas credit card. No Japanese bank account needed",
highlight: "Easy payment",
},
{
icon: <Signal className="h-6 w-6" />,
title: "Japan's Best Network",
description: "NTT Docomo coverage reaches 99.9% of Japan's population",
highlight: "Nationwide",
},
{
icon: <CircleDollarSign className="h-6 w-6" />,
title: "First Month Free",
description: "Try risk-free. Your first month's basic fee is waived",
highlight: "Great value",
},
{
icon: <Calendar className="h-6 w-6" />,
title: "Flexible Terms",
description: "No multi-year contracts. Stay as long as you need",
highlight: "No lock-in",
},
{
icon: <ArrowRightLeft className="h-6 w-6" />,
title: "Keep Your Number",
description: "Switching from another carrier? Bring your Japanese number with you",
highlight: "Easy transfer",
},
{
icon: <Smartphone className="h-6 w-6" />,
title: "Free Plan Changes",
description: "Switch data plans anytime for the next billing cycle",
highlight: "Flexibility",
},
];
if (isLoading) {
return (
<div className="max-w-5xl mx-auto px-4 pb-16 pt-6">
@ -324,29 +348,7 @@ export function SimPlansContent({
{/* 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 => (
{SIM_TABS.map(tab => (
<button
key={tab.key}
type="button"
@ -362,7 +364,7 @@ export function SimPlansContent({
<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}
{plansByType[tab.planTypeKey].length}
</span>
</button>
))}
@ -631,54 +633,7 @@ export function SimPlansContent({
</div>
{/* FAQ Section */}
<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."
/>
<FaqItem
question="I've changed my phone and the SIM card is not working on the new device."
answer="Whenever the SIM card is used with a new device, the APN profile would need to be installed on said device. Please refer to the APN Setup Guide in the Documents section to check how the profile can be installed."
/>
<FaqItem
question="Are international calling features available?"
answer={
<>
Enter &quot;+&quot; or &quot;010&quot;, &quot;recipient&apos;s country code&quot;,
and &quot;recipient&apos;s phone number (regular phone number/mobile phone
number)&quot; Make a call.
<br />
<br />
If the recipient&apos;s phone number begins with a 0, enter it without the first 0
(except in some countries and regions).
<br />
International calling rate is on the following Docomo&apos;s website:{" "}
<a
href="https://www.docomo.ne.jp/english/service/world/roaming/charges/kaigai/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Docomo International Calling Rates
</a>
</>
}
/>
<FaqItem
question="How do I cancel the service?"
answer="To cancel, please log into the SonixNet SIM Management Website and send us a cancellation request before the 25th to cancel your account at the end of the month. For example, cancellation requests will need to be sent in by May 25th, in order to cancel at the end of May."
/>
</div>
</div>
<ServiceFAQ items={SIM_FAQ_ITEMS} />
{/* Footer */}
<div className="text-center text-xs text-muted-foreground">
@ -693,31 +648,4 @@ export function SimPlansContent({
);
}
function FaqItem({ question, answer }: { question: string; answer: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<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 group"
>
<span className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
{question}
</span>
<ChevronDown
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-xs text-muted-foreground leading-relaxed">{answer}</div>
)}
</div>
);
}
export default SimPlansContent;

View File

@ -1,6 +1,7 @@
"use client";
import { ShieldCheck, Zap, CreditCard, Play, Globe, Package, ArrowLeft } from "lucide-react";
import { ServicesHero } from "@/features/services/components/base/ServicesHero";
import { Skeleton } from "@/components/atoms/loading-skeleton";
import { Button } from "@/components/atoms/button";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
@ -135,35 +136,18 @@ export function VpnPlansContent({
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
{/* 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-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">
<ServicesHero
title="Stream Content from Abroad"
description="Access US and UK streaming services using a pre-configured VPN router. No technical setup required."
eyebrow={
<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">
<ShieldCheck className="h-3.5 w-3.5" />
VPN Router Service
</span>
</div>
<h1
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-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>
}
>
{variant === "public" && (
<div
className="inline-flex mt-4 animate-in fade-in slide-in-from-bottom-8 duration-700"
style={{ animationDelay: "300ms" }}
>
<div className="flex justify-center">
<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" />
@ -178,7 +162,7 @@ export function VpnPlansContent({
</div>
</div>
)}
</div>
</ServicesHero>
{/* Highlights */}
<ServiceHighlights features={VPN_FEATURES} />

View File

@ -142,6 +142,14 @@
font-family are explicit Tailwind classes at the call site so they
can be overridden without cascade conflicts. */
@utility full-bleed {
position: relative;
left: 50%;
right: 50%;
width: 100vw;
transform: translateX(-50%);
}
@utility text-display-xl {
font-size: var(--cp-text-display-xl);
letter-spacing: var(--cp-tracking-tight);