refactor: update layout and components for improved consistency and accessibility
- Replaced the "About us.png" image with a new "about-us.png" for better naming consistency. - Updated various sections across the landing page to use a full-bleed layout for a more modern design. - Refactored ServiceCard component to conditionally render as a link or div based on the presence of an href prop, enhancing flexibility. - Introduced a new CollapsibleSection component for better organization of content in service-related sections. - Enhanced styling and structure in multiple components, including ContactSection, CTABanner, and TrustStrip, to improve visual hierarchy and user experience.
This commit is contained in:
parent
48eb8c8725
commit
7125f79baa
|
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 426 KiB |
@ -106,7 +106,7 @@ export function OnsiteSupportContent() {
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Hero Section */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 overflow-hidden bg-gradient-to-br from-slate-50 via-white to-sky-50/80 pt-10 pb-20">
|
||||
<section className="full-bleed overflow-hidden bg-gradient-to-br from-slate-50 via-white to-sky-50/80 pt-10 pb-20">
|
||||
{/* Dot grid pattern */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
@ -195,7 +195,7 @@ export function OnsiteSupportContent() {
|
||||
</section>
|
||||
|
||||
{/* How It Works Section */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-16">
|
||||
<section className="full-bleed bg-white py-16">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
|
||||
How It Works
|
||||
@ -239,7 +239,7 @@ export function OnsiteSupportContent() {
|
||||
</section>
|
||||
|
||||
{/* Pricing Cards Section */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-16">
|
||||
<section className="full-bleed bg-[#f7f7f7] py-16">
|
||||
{/* Subtle pattern overlay */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none opacity-30"
|
||||
@ -335,7 +335,7 @@ export function OnsiteSupportContent() {
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-16">
|
||||
<section className="full-bleed bg-white py-16">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
|
||||
Frequently Asked Questions
|
||||
@ -387,7 +387,7 @@ export function OnsiteSupportContent() {
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-16">
|
||||
<section className="full-bleed bg-[#f7f7f7] py-16">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2 text-sm text-primary font-medium mb-6">
|
||||
<Shield className="h-4 w-4" />
|
||||
|
||||
@ -35,8 +35,8 @@ export type ServiceCardVariant =
|
||||
| "bento-lg";
|
||||
|
||||
export interface ServiceCardProps {
|
||||
/** Link destination */
|
||||
href: string;
|
||||
/** Link destination (renders as div when omitted) */
|
||||
href?: string;
|
||||
/**
|
||||
* Icon element to display.
|
||||
* Pass a pre-styled JSX element: `icon={<Wifi className="h-6 w-6" />}`
|
||||
@ -157,6 +157,28 @@ function renderIcon(icon: ReactNode, className: string): ReactNode {
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared wrapper that renders <Link> when href is provided, <div> otherwise
|
||||
*/
|
||||
function CardWrapper({
|
||||
href,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
href?: string | undefined;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} className={className}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default variant - Standard service card
|
||||
*/
|
||||
@ -173,7 +195,7 @@ function DefaultCard({
|
||||
const colors = accentColorStyles[accentColor];
|
||||
|
||||
return (
|
||||
<Link href={href} className={cn("group block", className)}>
|
||||
<CardWrapper href={href} className={cn("group block", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full flex flex-col rounded-2xl border bg-card p-6",
|
||||
@ -212,12 +234,14 @@ function DefaultCard({
|
||||
<p className="text-sm text-muted-foreground leading-relaxed flex-grow">{description}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex items-center gap-1 text-sm font-medium text-primary">
|
||||
Learn more
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</div>
|
||||
{href && (
|
||||
<div className="mt-4 flex items-center gap-1 text-sm font-medium text-primary">
|
||||
Learn more
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -226,7 +250,7 @@ function DefaultCard({
|
||||
*/
|
||||
function FeaturedCard({ href, icon, title, description, highlight, className }: ServiceCardProps) {
|
||||
return (
|
||||
<Link href={href} className={cn("group block h-full", className)}>
|
||||
<CardWrapper href={href} className={cn("group block h-full", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex flex-col",
|
||||
@ -269,19 +293,21 @@ function FeaturedCard({ href, icon, title, description, highlight, className }:
|
||||
)}
|
||||
|
||||
{/* Link indicator */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 mt-4 pt-4 border-t",
|
||||
"text-sm font-medium text-primary",
|
||||
"transition-colors group-hover:text-primary-hover",
|
||||
"border-primary/10"
|
||||
)}
|
||||
>
|
||||
Learn more
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</div>
|
||||
{href && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 mt-4 pt-4 border-t",
|
||||
"text-sm font-medium text-primary",
|
||||
"transition-colors group-hover:text-primary-hover",
|
||||
"border-primary/10"
|
||||
)}
|
||||
>
|
||||
Learn more
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -290,7 +316,7 @@ function FeaturedCard({ href, icon, title, description, highlight, className }:
|
||||
*/
|
||||
function MinimalCard({ href, icon, title, className }: ServiceCardProps) {
|
||||
return (
|
||||
<Link href={href} className={cn("group block", className)}>
|
||||
<CardWrapper href={href} className={cn("group block", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center text-center",
|
||||
@ -306,7 +332,7 @@ function MinimalCard({ href, icon, title, className }: ServiceCardProps) {
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-foreground font-display">{title}</h3>
|
||||
</div>
|
||||
</Link>
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -317,7 +343,7 @@ function BentoSmallCard({ href, icon, title, accentColor = "blue", className }:
|
||||
const colors = accentColorStyles[accentColor];
|
||||
|
||||
return (
|
||||
<Link
|
||||
<CardWrapper
|
||||
href={href}
|
||||
className={cn(
|
||||
"group rounded-xl bg-card/80 backdrop-blur-sm border border-border/50",
|
||||
@ -333,7 +359,7 @@ function BentoSmallCard({ href, icon, title, accentColor = "blue", className }:
|
||||
</div>
|
||||
<span className="font-semibold text-foreground">{title}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -351,7 +377,7 @@ function BentoMediumCard({
|
||||
const colors = accentColorStyles[accentColor];
|
||||
|
||||
return (
|
||||
<Link
|
||||
<CardWrapper
|
||||
href={href}
|
||||
className={cn(
|
||||
"group rounded-xl bg-card border",
|
||||
@ -377,7 +403,7 @@ function BentoMediumCard({
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{description}</p>
|
||||
)}
|
||||
</Link>
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -395,7 +421,7 @@ function BentoLargeCard({
|
||||
const colors = accentColorStyles[accentColor];
|
||||
|
||||
return (
|
||||
<Link
|
||||
<CardWrapper
|
||||
href={href}
|
||||
className={cn(
|
||||
"group relative overflow-hidden rounded-2xl",
|
||||
@ -437,19 +463,21 @@ function BentoLargeCard({
|
||||
<p className="text-muted-foreground leading-relaxed max-w-sm mb-6">{description}</p>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 font-semibold",
|
||||
colors.text,
|
||||
"transition-transform duration-[var(--cp-duration-normal)]",
|
||||
"group-hover:translate-x-1"
|
||||
)}
|
||||
>
|
||||
Learn more
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</span>
|
||||
{href && (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 font-semibold",
|
||||
colors.text,
|
||||
"transition-transform duration-[var(--cp-duration-normal)]",
|
||||
"group-hover:translate-x-1"
|
||||
)}
|
||||
>
|
||||
Learn more
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,10 +3,7 @@ import { Button } from "@/components/atoms/button";
|
||||
|
||||
export function CTABanner() {
|
||||
return (
|
||||
<section
|
||||
aria-label="Call to action"
|
||||
className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-soft"
|
||||
>
|
||||
<section aria-label="Call to action" className="full-bleed bg-primary-soft">
|
||||
<div className="mx-auto max-w-3xl px-6 sm:px-10 lg:px-14 py-14 sm:py-16 text-center">
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-foreground">
|
||||
Ready to Get Set Up?
|
||||
|
||||
@ -13,7 +13,7 @@ export function ContactSection() {
|
||||
id="contact"
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/30 py-14 sm:py-16 transition-all duration-700",
|
||||
"full-bleed bg-surface-sunken/30 py-14 sm:py-16 transition-all duration-700",
|
||||
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
)}
|
||||
>
|
||||
|
||||
@ -16,7 +16,7 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
|
||||
<section
|
||||
ref={heroRef}
|
||||
className={cn(
|
||||
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-16 sm:py-20 lg:py-24 overflow-hidden transition-all duration-700",
|
||||
"full-bleed py-16 sm:py-20 lg:py-24 overflow-hidden transition-all duration-700",
|
||||
heroInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
)}
|
||||
>
|
||||
|
||||
@ -299,7 +299,7 @@ export function ServicesCarousel() {
|
||||
<section
|
||||
ref={sectionRef}
|
||||
className={cn(
|
||||
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/30 py-16 sm:py-20 transition-all duration-700",
|
||||
"full-bleed bg-surface-sunken/30 py-16 sm:py-20 transition-all duration-700",
|
||||
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
)}
|
||||
>
|
||||
|
||||
@ -73,7 +73,7 @@ export function TrustStrip() {
|
||||
ref={ref}
|
||||
aria-label="Company statistics"
|
||||
className={cn(
|
||||
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-10 sm:py-12 overflow-hidden transition-all duration-700",
|
||||
"full-bleed py-10 sm:py-12 overflow-hidden transition-all duration-700",
|
||||
inView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
)}
|
||||
>
|
||||
|
||||
@ -19,7 +19,7 @@ export function WhyUsSection() {
|
||||
<section
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-background transition-all duration-700",
|
||||
"full-bleed bg-background transition-all duration-700",
|
||||
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
)}
|
||||
>
|
||||
|
||||
@ -4,7 +4,7 @@ export function PublicLandingLoadingView() {
|
||||
return (
|
||||
<div className="space-y-0 pb-8 pt-0">
|
||||
{/* Hero Section Skeleton */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken py-16 sm:py-20 lg:py-24">
|
||||
<section className="full-bleed bg-surface-sunken py-16 sm:py-20 lg:py-24">
|
||||
<div className="mx-auto max-w-3xl px-6 sm:px-10 lg:px-14 text-center space-y-6">
|
||||
<div className="space-y-3 mx-auto max-w-lg">
|
||||
<Skeleton className="h-10 w-72 mx-auto rounded-md" />
|
||||
@ -22,7 +22,7 @@ export function PublicLandingLoadingView() {
|
||||
</section>
|
||||
|
||||
{/* TrustStrip Skeleton */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/50 border-y border-border/40">
|
||||
<section className="full-bleed bg-surface-sunken/50 border-y border-border/40">
|
||||
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-14 py-5">
|
||||
<div className="grid grid-cols-2 gap-4 sm:flex sm:justify-between">
|
||||
{Array.from({ length: 4 }).map((_, idx) => (
|
||||
@ -72,7 +72,7 @@ export function PublicLandingLoadingView() {
|
||||
</section>
|
||||
|
||||
{/* CTABanner Skeleton */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-soft">
|
||||
<section className="full-bleed bg-primary-soft">
|
||||
<div className="mx-auto max-w-3xl px-6 sm:px-10 py-14 sm:py-16 text-center space-y-4">
|
||||
<Skeleton className="h-8 w-64 mx-auto rounded-md" />
|
||||
<Skeleton className="h-4 w-80 mx-auto rounded-md" />
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import Image from "next/image";
|
||||
import { ServiceCard } from "@/components/molecules";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Wifi,
|
||||
@ -21,34 +22,39 @@ import {
|
||||
|
||||
/* ─── Data ─── */
|
||||
|
||||
const services: { title: string; description: string; icon: LucideIcon }[] = [
|
||||
const services = [
|
||||
{
|
||||
title: "Internet Plans",
|
||||
description:
|
||||
"High-speed NTT fiber with English support. We handle the Japanese paperwork so you don't have to.",
|
||||
icon: Wifi,
|
||||
icon: <Wifi />,
|
||||
accentColor: "blue" as const,
|
||||
},
|
||||
{
|
||||
title: "Phone Plans",
|
||||
description:
|
||||
"SIM cards on Japan's best network. Foreign credit cards accepted, no hanko required.",
|
||||
icon: Smartphone,
|
||||
icon: <Smartphone />,
|
||||
accentColor: "green" as const,
|
||||
},
|
||||
{
|
||||
title: "Business Solutions",
|
||||
description:
|
||||
"Enterprise IT for international companies. Dedicated internet, office networks, and data centers.",
|
||||
icon: Building2,
|
||||
icon: <Building2 />,
|
||||
accentColor: "purple" as const,
|
||||
},
|
||||
{
|
||||
title: "VPN",
|
||||
description: "Stream your favorite shows from home. Pre-configured router for US/UK content.",
|
||||
icon: Lock,
|
||||
icon: <Lock />,
|
||||
accentColor: "orange" as const,
|
||||
},
|
||||
{
|
||||
title: "Onsite Support",
|
||||
description: "English-speaking technicians at your door for setup and troubleshooting.",
|
||||
icon: Wrench,
|
||||
icon: <Wrench />,
|
||||
accentColor: "cyan" as const,
|
||||
},
|
||||
];
|
||||
|
||||
@ -111,10 +117,6 @@ const businessHours = [
|
||||
{ team: "Onsite Tech Support Team", hours: "Mon \u2013 Sat 10:00AM \u2013 9:00PM" },
|
||||
];
|
||||
|
||||
/* ─── Full-width section wrapper ─── */
|
||||
|
||||
const SECTION_BASE = "relative left-1/2 right-1/2 w-screen -translate-x-1/2";
|
||||
|
||||
const HERO_GLOW_STYLE = {
|
||||
background: "radial-gradient(circle, oklch(0.7 0.12 234.4 / 0.35), transparent 70%)",
|
||||
} as const;
|
||||
@ -123,7 +125,7 @@ const HERO_GLOW_STYLE = {
|
||||
|
||||
function HeroSection() {
|
||||
return (
|
||||
<section className={`${SECTION_BASE} overflow-hidden bg-surface-sunken`}>
|
||||
<section className="full-bleed overflow-hidden bg-surface-sunken">
|
||||
<div
|
||||
className="pointer-events-none absolute -top-1/3 right-0 h-[600px] w-[600px] opacity-30"
|
||||
style={HERO_GLOW_STYLE}
|
||||
@ -152,7 +154,7 @@ function HeroSection() {
|
||||
</div>
|
||||
<div className="relative mx-auto h-[380px] w-full max-w-md lg:h-[440px] lg:max-w-none">
|
||||
<Image
|
||||
src="/assets/images/About us.png"
|
||||
src="/assets/images/about-us.png"
|
||||
alt="Assist Solutions team in Tokyo"
|
||||
fill
|
||||
priority
|
||||
@ -168,7 +170,7 @@ function HeroSection() {
|
||||
|
||||
function ServicesSection() {
|
||||
return (
|
||||
<section className={`${SECTION_BASE} bg-background py-16 sm:py-20`}>
|
||||
<section className="full-bleed bg-background py-16 sm:py-20">
|
||||
<div className="mx-auto max-w-6xl px-6 sm:px-8">
|
||||
<div className="cp-stagger-children mb-10 max-w-xl">
|
||||
<h2 className="text-display-sm font-bold font-display text-foreground">What We Do</h2>
|
||||
@ -179,23 +181,16 @@ function ServicesSection() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{services.map(service => {
|
||||
const Icon = service.icon;
|
||||
return (
|
||||
<div
|
||||
key={service.title}
|
||||
className="group cp-card-hover-lift rounded-2xl border border-border/60 bg-card p-6 hover:border-primary/20"
|
||||
>
|
||||
<div className="mb-4 inline-flex h-11 w-11 items-center justify-center rounded-xl bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-foreground">{service.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{services.map(service => (
|
||||
<ServiceCard
|
||||
key={service.title}
|
||||
variant="bento-md"
|
||||
icon={service.icon}
|
||||
title={service.title}
|
||||
description={service.description}
|
||||
accentColor={service.accentColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -204,7 +199,7 @@ function ServicesSection() {
|
||||
|
||||
function ValuesSection() {
|
||||
return (
|
||||
<section className={`${SECTION_BASE} bg-surface-sunken py-16 sm:py-20`}>
|
||||
<section className="full-bleed bg-surface-sunken py-16 sm:py-20">
|
||||
<div className="mx-auto max-w-6xl px-6 sm:px-8">
|
||||
<div className="cp-stagger-children mb-10 text-center">
|
||||
<h2 className="text-display-sm font-bold font-display text-foreground">Our Values</h2>
|
||||
@ -242,7 +237,7 @@ function ValuesSection() {
|
||||
|
||||
function CorporateSection() {
|
||||
return (
|
||||
<section className={`${SECTION_BASE} bg-background py-16 sm:py-20`}>
|
||||
<section className="full-bleed bg-background py-16 sm:py-20">
|
||||
<div className="mx-auto max-w-6xl px-6 sm:px-8">
|
||||
<div className="cp-stagger-children mb-10">
|
||||
<h2 className="text-display-sm font-bold font-display text-foreground">Corporate Data</h2>
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type ElementType, type ReactNode } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/shared/utils";
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
icon: ElementType;
|
||||
defaultOpen?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function CollapsibleSection({
|
||||
title,
|
||||
icon: Icon,
|
||||
defaultOpen = false,
|
||||
children,
|
||||
}: CollapsibleSectionProps) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div className="border border-border/60 rounded-xl overflow-hidden bg-card">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Icon className="w-4 h-4 text-primary" />
|
||||
<span className="text-sm font-medium text-foreground">{title}</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
||||
isOpen && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden transition-all duration-300",
|
||||
isOpen ? "max-h-[2000px]" : "max-h-0"
|
||||
)}
|
||||
>
|
||||
<div className="p-4 pt-0 border-t border-border/60">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -24,6 +24,11 @@ export interface ServiceCTAProps {
|
||||
className?: string | undefined;
|
||||
}
|
||||
|
||||
const dotPatternStyle = {
|
||||
backgroundImage: `radial-gradient(circle at center, color-mix(in oklch, var(--primary) 12%, transparent) 0.6px, transparent 0.6px)`,
|
||||
backgroundSize: "20px 20px",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ServiceCTA - Call-to-action section with decorative background.
|
||||
*/
|
||||
@ -42,13 +47,7 @@ export function ServiceCTA({
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-primary/8" />
|
||||
|
||||
{/* Refined dot pattern */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none opacity-30"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle at center, color-mix(in oklch, var(--primary) 12%, transparent) 0.6px, transparent 0.6px)`,
|
||||
backgroundSize: "20px 20px",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 pointer-events-none opacity-30" style={dotPatternStyle} />
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative">
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
export interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
answer: ReactNode;
|
||||
}
|
||||
|
||||
export interface ServiceFAQProps {
|
||||
@ -24,7 +24,7 @@ function FAQItemComponent({
|
||||
onToggle,
|
||||
}: {
|
||||
question: string;
|
||||
answer: string;
|
||||
answer: ReactNode;
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
@ -53,7 +53,7 @@ function FAQItemComponent({
|
||||
)}
|
||||
>
|
||||
<div className="pb-4 pr-8">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{answer}</p>
|
||||
<div className="text-sm text-muted-foreground leading-relaxed">{answer}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, type ElementType, type ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Smartphone,
|
||||
Check,
|
||||
@ -10,7 +10,6 @@ import {
|
||||
Signal,
|
||||
Sparkles,
|
||||
CreditCard,
|
||||
ChevronDown,
|
||||
Info,
|
||||
CircleDollarSign,
|
||||
TriangleAlert,
|
||||
@ -27,6 +26,8 @@ import { ServicesBackLink } from "@/features/services/components/base/ServicesBa
|
||||
import { ServicesHero } from "@/features/services/components/base/ServicesHero";
|
||||
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
||||
import { CardPricing } from "@/features/services/components/base/CardPricing";
|
||||
import { CollapsibleSection } from "@/features/services/components/base/CollapsibleSection";
|
||||
import { ServiceFAQ, type FAQItem } from "@/features/services/components/base/ServiceFAQ";
|
||||
import { DeviceCompatibility } from "./DeviceCompatibility";
|
||||
import {
|
||||
ServiceHighlights,
|
||||
@ -43,48 +44,110 @@ interface PlansByType {
|
||||
VoiceOnly: SimCatalogProduct[];
|
||||
}
|
||||
|
||||
function CollapsibleSection({
|
||||
title,
|
||||
icon: Icon,
|
||||
defaultOpen = false,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
icon: ElementType;
|
||||
defaultOpen?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const simFeatures: HighlightFeature[] = [
|
||||
{
|
||||
icon: <CreditCard className="h-6 w-6" />,
|
||||
title: "Foreign Cards Accepted",
|
||||
description: "Use your overseas credit card. No Japanese bank account needed",
|
||||
highlight: "Easy payment",
|
||||
},
|
||||
{
|
||||
icon: <Signal className="h-6 w-6" />,
|
||||
title: "Japan's Best Network",
|
||||
description: "NTT Docomo coverage reaches 99.9% of Japan's population",
|
||||
highlight: "Nationwide",
|
||||
},
|
||||
{
|
||||
icon: <CircleDollarSign className="h-6 w-6" />,
|
||||
title: "First Month Free",
|
||||
description: "Try risk-free. Your first month's basic fee is waived",
|
||||
highlight: "Great value",
|
||||
},
|
||||
{
|
||||
icon: <Calendar className="h-6 w-6" />,
|
||||
title: "Flexible Terms",
|
||||
description: "No multi-year contracts. Stay as long as you need",
|
||||
highlight: "No lock-in",
|
||||
},
|
||||
{
|
||||
icon: <ArrowRightLeft className="h-6 w-6" />,
|
||||
title: "Keep Your Number",
|
||||
description: "Switching from another carrier? Bring your Japanese number with you",
|
||||
highlight: "Easy transfer",
|
||||
},
|
||||
{
|
||||
icon: <Smartphone className="h-6 w-6" />,
|
||||
title: "Free Plan Changes",
|
||||
description: "Switch data plans anytime for the next billing cycle",
|
||||
highlight: "Flexibility",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border border-border/60 rounded-xl overflow-hidden bg-card">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Icon className="w-4 h-4 text-primary" />
|
||||
<span className="text-sm font-medium text-foreground">{title}</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
||||
isOpen && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden transition-all duration-300",
|
||||
isOpen ? "max-h-[2000px]" : "max-h-0"
|
||||
)}
|
||||
>
|
||||
<div className="p-4 pt-0 border-t border-border/60">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const SIM_TABS = [
|
||||
{
|
||||
key: "data-voice" as const,
|
||||
icon: Phone,
|
||||
label: "Data + Voice",
|
||||
shortLabel: "All-in",
|
||||
planTypeKey: "DataSmsVoice" as const,
|
||||
},
|
||||
{
|
||||
key: "data-only" as const,
|
||||
icon: Globe,
|
||||
label: "Data Only",
|
||||
shortLabel: "Data",
|
||||
planTypeKey: "DataOnly" as const,
|
||||
},
|
||||
{
|
||||
key: "voice-only" as const,
|
||||
icon: Check,
|
||||
label: "Voice Only",
|
||||
shortLabel: "Voice",
|
||||
planTypeKey: "VoiceOnly" as const,
|
||||
},
|
||||
];
|
||||
|
||||
const SIM_FAQ_ITEMS: FAQItem[] = [
|
||||
{
|
||||
question: "What is the service contract period?",
|
||||
answer:
|
||||
"The minimum service requirement period is activation month + 3 billing months. After this period, the service will switch to a monthly service and you will be able to cancel at the end of each month.",
|
||||
},
|
||||
{
|
||||
question: "I've changed my phone and the SIM card is not working on the new device.",
|
||||
answer:
|
||||
"Whenever the SIM card is used with a new device, the APN profile would need to be installed on said device. Please refer to the APN Setup Guide in the Documents section to check how the profile can be installed.",
|
||||
},
|
||||
{
|
||||
question: "Are international calling features available?",
|
||||
answer: (
|
||||
<>
|
||||
Enter "+" or "010", "recipient's country code", and
|
||||
"recipient's phone number (regular phone number/mobile phone number)" → Make
|
||||
a call.
|
||||
<br />
|
||||
<br />
|
||||
If the recipient's phone number begins with a 0, enter it without the first 0 (except
|
||||
in some countries and regions).
|
||||
<br />
|
||||
International calling rate is on the following Docomo's website:{" "}
|
||||
<a
|
||||
href="https://www.docomo.ne.jp/english/service/world/roaming/charges/kaigai/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Docomo International Calling Rates
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "How do I cancel the service?",
|
||||
answer:
|
||||
"To cancel, please log into the SonixNet SIM Management Website and send us a cancellation request before the 25th to cancel your account at the end of the month. For example, cancellation requests will need to be sent in by May 25th, in order to cancel at the end of May.",
|
||||
},
|
||||
];
|
||||
|
||||
function SimPlanCardCompact({
|
||||
plan,
|
||||
@ -187,45 +250,6 @@ export function SimPlansContent({
|
||||
const simPlans: SimCatalogProduct[] = useMemo(() => plans ?? [], [plans]);
|
||||
const hasExistingSim = useMemo(() => simPlans.some(p => p.simHasFamilyDiscount), [simPlans]);
|
||||
|
||||
const simFeatures: HighlightFeature[] = [
|
||||
{
|
||||
icon: <CreditCard className="h-6 w-6" />,
|
||||
title: "Foreign Cards Accepted",
|
||||
description: "Use your overseas credit card. No Japanese bank account needed",
|
||||
highlight: "Easy payment",
|
||||
},
|
||||
{
|
||||
icon: <Signal className="h-6 w-6" />,
|
||||
title: "Japan's Best Network",
|
||||
description: "NTT Docomo coverage reaches 99.9% of Japan's population",
|
||||
highlight: "Nationwide",
|
||||
},
|
||||
{
|
||||
icon: <CircleDollarSign className="h-6 w-6" />,
|
||||
title: "First Month Free",
|
||||
description: "Try risk-free. Your first month's basic fee is waived",
|
||||
highlight: "Great value",
|
||||
},
|
||||
{
|
||||
icon: <Calendar className="h-6 w-6" />,
|
||||
title: "Flexible Terms",
|
||||
description: "No multi-year contracts. Stay as long as you need",
|
||||
highlight: "No lock-in",
|
||||
},
|
||||
{
|
||||
icon: <ArrowRightLeft className="h-6 w-6" />,
|
||||
title: "Keep Your Number",
|
||||
description: "Switching from another carrier? Bring your Japanese number with you",
|
||||
highlight: "Easy transfer",
|
||||
},
|
||||
{
|
||||
icon: <Smartphone className="h-6 w-6" />,
|
||||
title: "Free Plan Changes",
|
||||
description: "Switch data plans anytime for the next billing cycle",
|
||||
highlight: "Flexibility",
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-4 pb-16 pt-6">
|
||||
@ -324,29 +348,7 @@ export function SimPlansContent({
|
||||
{/* Tab Switcher */}
|
||||
<div className="flex justify-center">
|
||||
<div className="inline-flex rounded-lg bg-muted/60 p-0.5 border border-border/60">
|
||||
{[
|
||||
{
|
||||
key: "data-voice" as const,
|
||||
icon: Phone,
|
||||
label: "Data + Voice",
|
||||
shortLabel: "All-in",
|
||||
count: plansByType.DataSmsVoice.length,
|
||||
},
|
||||
{
|
||||
key: "data-only" as const,
|
||||
icon: Globe,
|
||||
label: "Data Only",
|
||||
shortLabel: "Data",
|
||||
count: plansByType.DataOnly.length,
|
||||
},
|
||||
{
|
||||
key: "voice-only" as const,
|
||||
icon: Check,
|
||||
label: "Voice Only",
|
||||
shortLabel: "Voice",
|
||||
count: plansByType.VoiceOnly.length,
|
||||
},
|
||||
].map(tab => (
|
||||
{SIM_TABS.map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
@ -362,7 +364,7 @@ export function SimPlansContent({
|
||||
<span className="hidden sm:inline">{tab.label}</span>
|
||||
<span className="sm:hidden">{tab.shortLabel}</span>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/8 text-primary font-semibold">
|
||||
{tab.count}
|
||||
{plansByType[tab.planTypeKey].length}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
@ -631,54 +633,7 @@ export function SimPlansContent({
|
||||
</div>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div>
|
||||
<div className="text-center mb-5">
|
||||
<p className="text-xs font-semibold text-primary uppercase tracking-wider mb-1.5">
|
||||
Common Questions
|
||||
</p>
|
||||
<h2 className="text-xl sm:text-2xl font-bold leading-tight tracking-tight text-foreground">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2 max-w-3xl mx-auto">
|
||||
<FaqItem
|
||||
question="What is the service contract period?"
|
||||
answer="The minimum service requirement period is activation month + 3 billing months. After this period, the service will switch to a monthly service and you will be able to cancel at the end of each month."
|
||||
/>
|
||||
<FaqItem
|
||||
question="I've changed my phone and the SIM card is not working on the new device."
|
||||
answer="Whenever the SIM card is used with a new device, the APN profile would need to be installed on said device. Please refer to the APN Setup Guide in the Documents section to check how the profile can be installed."
|
||||
/>
|
||||
<FaqItem
|
||||
question="Are international calling features available?"
|
||||
answer={
|
||||
<>
|
||||
Enter "+" or "010", "recipient's country code",
|
||||
and "recipient's phone number (regular phone number/mobile phone
|
||||
number)" → Make a call.
|
||||
<br />
|
||||
<br />
|
||||
If the recipient's phone number begins with a 0, enter it without the first 0
|
||||
(except in some countries and regions).
|
||||
<br />
|
||||
International calling rate is on the following Docomo's website:{" "}
|
||||
<a
|
||||
href="https://www.docomo.ne.jp/english/service/world/roaming/charges/kaigai/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Docomo International Calling Rates
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FaqItem
|
||||
question="How do I cancel the service?"
|
||||
answer="To cancel, please log into the SonixNet SIM Management Website and send us a cancellation request before the 25th to cancel your account at the end of the month. For example, cancellation requests will need to be sent in by May 25th, in order to cancel at the end of May."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ServiceFAQ items={SIM_FAQ_ITEMS} />
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center text-xs text-muted-foreground">
|
||||
@ -693,31 +648,4 @@ export function SimPlansContent({
|
||||
);
|
||||
}
|
||||
|
||||
function FaqItem({ question, answer }: { question: string; answer: React.ReactNode }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="border border-border/60 rounded-xl overflow-hidden bg-card">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full flex items-start justify-between gap-3 p-4 text-left group"
|
||||
>
|
||||
<span className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
|
||||
{question}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"w-4 h-4 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200",
|
||||
isOpen && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="px-4 pb-4 text-xs text-muted-foreground leading-relaxed">{answer}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SimPlansContent;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ShieldCheck, Zap, CreditCard, Play, Globe, Package, ArrowLeft } from "lucide-react";
|
||||
import { ServicesHero } from "@/features/services/components/base/ServicesHero";
|
||||
import { Skeleton } from "@/components/atoms/loading-skeleton";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
@ -135,35 +136,18 @@ export function VpnPlansContent({
|
||||
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
|
||||
|
||||
{/* Hero */}
|
||||
<div className="text-center py-4">
|
||||
<div
|
||||
className="animate-in fade-in slide-in-from-bottom-4 duration-500"
|
||||
style={{ animationDelay: "0ms" }}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-violet-500/10 border border-violet-500/15 px-3 py-1 text-xs text-violet-600 dark:text-violet-400 font-medium mb-3">
|
||||
<ServicesHero
|
||||
title="Stream Content from Abroad"
|
||||
description="Access US and UK streaming services using a pre-configured VPN router. No technical setup required."
|
||||
eyebrow={
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-violet-500/10 border border-violet-500/15 px-3 py-1 text-xs text-violet-600 dark:text-violet-400 font-medium">
|
||||
<ShieldCheck className="h-3.5 w-3.5" />
|
||||
VPN Router Service
|
||||
</span>
|
||||
</div>
|
||||
<h1
|
||||
className="text-2xl md:text-3xl font-bold leading-tight tracking-tight text-foreground animate-in fade-in slide-in-from-bottom-6 duration-700"
|
||||
style={{ animationDelay: "100ms" }}
|
||||
>
|
||||
Stream Content from Abroad
|
||||
</h1>
|
||||
<p
|
||||
className="text-sm text-muted-foreground mt-2 max-w-lg mx-auto leading-relaxed animate-in fade-in slide-in-from-bottom-8 duration-700"
|
||||
style={{ animationDelay: "200ms" }}
|
||||
>
|
||||
Access US and UK streaming services using a pre-configured VPN router. No technical setup
|
||||
required.
|
||||
</p>
|
||||
|
||||
}
|
||||
>
|
||||
{variant === "public" && (
|
||||
<div
|
||||
className="inline-flex mt-4 animate-in fade-in slide-in-from-bottom-8 duration-700"
|
||||
style={{ animationDelay: "300ms" }}
|
||||
>
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-success-soft border border-success/20 rounded-lg px-3 py-2">
|
||||
<div className="flex items-center gap-1.5 justify-center">
|
||||
<Zap className="h-3.5 w-3.5 text-success flex-shrink-0" />
|
||||
@ -178,7 +162,7 @@ export function VpnPlansContent({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ServicesHero>
|
||||
|
||||
{/* Highlights */}
|
||||
<ServiceHighlights features={VPN_FEATURES} />
|
||||
|
||||
@ -142,6 +142,14 @@
|
||||
font-family are explicit Tailwind classes at the call site so they
|
||||
can be overridden without cascade conflicts. */
|
||||
|
||||
@utility full-bleed {
|
||||
position: relative;
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
width: 100vw;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@utility text-display-xl {
|
||||
font-size: var(--cp-text-display-xl);
|
||||
letter-spacing: var(--cp-tracking-tight);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user