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:
parent
d5294dc580
commit
48eb8c8725
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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'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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'll check what'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>{" "}
|
||||
|
||||
@ -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" />;
|
||||
}
|
||||
|
||||
@ -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">•</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">•</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">•</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">•</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">•</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">•</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'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">•</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">•</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">•</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">•</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">•</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">•</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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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'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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user