refactor: update landing page components and styles

- Removed obsolete components including ContactSection, PopularServicesSection, RemoteSupportSection, SolutionsCarousel, and TrustSection to streamline the landing page.
- Introduced new components such as TrustStrip and ServicesGrid for improved layout and functionality.
- Enhanced global CSS with new line-height tokens and updated styles for better typography.
- Updated PublicLandingLoadingView for consistent loading states across the landing page.
- Added new landing services data structure to support the ServicesGrid component.
This commit is contained in:
barsa 2026-03-04 13:11:20 +09:00
parent 6b13d74d06
commit 0523bf80a8
23 changed files with 335 additions and 1208 deletions

View File

@ -316,6 +316,9 @@
/* Glass tokens */
--color-glass-bg: var(--glass-bg);
--color-glass-border: var(--glass-border);
/* Line-height tokens */
--leading-display: 1.1;
}
@layer base {

View File

@ -0,0 +1,40 @@
import Link from "next/link";
import { ArrowRight, Phone } from "lucide-react";
export function CTABanner() {
return (
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-soft">
<div className="mx-auto max-w-3xl px-6 sm:px-10 py-14 sm:py-16 text-center">
<h2 className="text-2xl sm:text-3xl font-extrabold text-foreground">
Ready to Get Connected?
</h2>
<p className="mt-2 text-base text-muted-foreground">
No Japanese required. Our English-speaking team is here to help.
</p>
<div className="mt-4 flex items-center justify-center gap-2 text-lg font-semibold text-primary">
<Phone className="h-5 w-5" />
<a href="tel:03-5812-1050" className="hover:underline">
03-5812-1050
</a>
</div>
<div className="mt-6 flex flex-col sm:flex-row items-center justify-center gap-3">
<Link
href="/services"
className="inline-flex items-center justify-center gap-2 rounded-full px-8 py-3 text-base font-semibold bg-primary text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 transition-transform hover:-translate-y-0.5"
>
Browse Services
<ArrowRight className="h-5 w-5" />
</Link>
<Link
href="/contact"
className="inline-flex items-center justify-center gap-2 rounded-full px-8 py-3 text-base font-semibold border border-border bg-card text-primary hover:bg-primary/5 transition-colors"
>
Contact Us
</Link>
</div>
</div>
</section>
);
}

View File

@ -1,302 +0,0 @@
"use client";
import {
Mail,
MessageSquare,
PhoneCall,
Train,
MapPin,
CheckCircle,
AlertCircle,
} from "lucide-react";
import { Spinner } from "@/components/atoms";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { CONTACT_SUBJECTS, type FormErrors } from "@/features/landing-page/data";
import { useInView, useContactForm } from "@/features/landing-page/hooks";
type ContactFormReturn = ReturnType<typeof useContactForm>;
interface ContactSectionProps {
formData: ContactFormReturn["formData"];
formErrors: ContactFormReturn["formErrors"];
formTouched: ContactFormReturn["formTouched"];
isSubmitting: ContactFormReturn["isSubmitting"];
submitStatus: ContactFormReturn["submitStatus"];
handleInputChange: ContactFormReturn["handleInputChange"];
handleInputBlur: ContactFormReturn["handleInputBlur"];
handleSubmit: ContactFormReturn["handleSubmit"];
}
function getInputClassName(
fieldName: keyof FormErrors,
formTouched: ContactSectionProps["formTouched"],
formErrors: ContactSectionProps["formErrors"]
) {
const baseClass =
"w-full rounded-md border px-4 py-3 text-sm focus-visible:outline-none focus-visible:ring-2 bg-card transition-colors";
const hasError = formTouched[fieldName] && formErrors[fieldName];
return `${baseClass} ${
hasError
? "border-danger focus-visible:ring-danger/60"
: "border-border focus-visible:ring-primary/60"
}`;
}
export function ContactSection({
formData,
formErrors,
formTouched,
isSubmitting,
submitStatus,
handleInputChange,
handleInputBlur,
handleSubmit,
}: ContactSectionProps) {
const [contactRef, contactInView] = useInView();
return (
<section
ref={contactRef as React.RefObject<HTMLElement>}
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-soft py-14 sm:py-16 transition-all duration-700 ${
contactInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
>
<div className="max-w-6xl mx-auto px-6 sm:px-10 lg:px-14 space-y-6">
<h2 className="text-3xl sm:text-4xl font-extrabold text-foreground">CONTACT US</h2>
<div className="rounded-3xl bg-card border border-primary/20 shadow-sm p-6 sm:p-8">
<div className="grid grid-cols-1 lg:grid-cols-[1.1fr_0.9fr] gap-10 lg:gap-12">
<div className="space-y-5">
<div className="flex items-center gap-2 text-primary font-bold text-lg">
<Mail className="h-5 w-5" />
<span>By Online Form (Anytime)</span>
</div>
{submitStatus === "success" && (
<AlertBanner variant="success" title="Message sent successfully!">
Thank you for contacting us. We&apos;ll get back to you within 24 hours.
</AlertBanner>
)}
{submitStatus === "error" && (
<AlertBanner variant="error" title="Failed to send message">
Please try again or contact us directly by phone.
</AlertBanner>
)}
<form className="space-y-4" onSubmit={handleSubmit} noValidate>
{/* Subject Dropdown */}
<div className="space-y-1">
<label htmlFor="contact-subject" className="sr-only">
Subject
</label>
<select
id="contact-subject"
name="subject"
value={formData.subject}
onChange={handleInputChange}
onBlur={handleInputBlur}
aria-required="true"
aria-invalid={formTouched.subject && !!formErrors.subject}
aria-describedby={formErrors.subject ? "subject-error" : undefined}
className={getInputClassName("subject", formTouched, formErrors)}
>
{CONTACT_SUBJECTS.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{formTouched.subject && formErrors.subject && (
<p id="subject-error" className="text-sm text-danger flex items-center gap-1">
<AlertCircle className="h-4 w-4" />
{formErrors.subject}
</p>
)}
</div>
{/* Name Input */}
<div className="space-y-1">
<label htmlFor="contact-name" className="sr-only">
Name
</label>
<input
id="contact-name"
type="text"
name="name"
placeholder="Name*"
value={formData.name}
onChange={handleInputChange}
onBlur={handleInputBlur}
aria-required="true"
aria-invalid={formTouched.name && !!formErrors.name}
aria-describedby={formErrors.name ? "name-error" : undefined}
className={getInputClassName("name", formTouched, formErrors)}
/>
{formTouched.name && formErrors.name && (
<p id="name-error" className="text-sm text-danger flex items-center gap-1">
<AlertCircle className="h-4 w-4" />
{formErrors.name}
</p>
)}
</div>
{/* Email Input */}
<div className="space-y-1">
<label htmlFor="contact-email" className="sr-only">
Email
</label>
<input
id="contact-email"
type="email"
name="email"
placeholder="Email*"
value={formData.email}
onChange={handleInputChange}
onBlur={handleInputBlur}
aria-required="true"
aria-invalid={formTouched.email && !!formErrors.email}
aria-describedby={formErrors.email ? "email-error" : undefined}
className={getInputClassName("email", formTouched, formErrors)}
/>
{formTouched.email && formErrors.email && (
<p id="email-error" className="text-sm text-danger flex items-center gap-1">
<AlertCircle className="h-4 w-4" />
{formErrors.email}
</p>
)}
</div>
{/* Phone Input (optional) */}
<div className="space-y-1">
<label htmlFor="contact-phone" className="sr-only">
Phone (optional)
</label>
<input
id="contact-phone"
type="tel"
name="phone"
placeholder="Phone (optional)"
value={formData.phone}
onChange={handleInputChange}
className="w-full rounded-md border border-border px-4 py-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60 bg-card"
/>
</div>
{/* Message Textarea */}
<div className="space-y-1">
<label htmlFor="contact-message" className="sr-only">
Inquiry
</label>
<textarea
id="contact-message"
name="message"
placeholder="Inquiry*"
rows={4}
value={formData.message}
onChange={handleInputChange}
onBlur={handleInputBlur}
aria-required="true"
aria-invalid={formTouched.message && !!formErrors.message}
aria-describedby={formErrors.message ? "message-error" : undefined}
className={`${getInputClassName("message", formTouched, formErrors)} resize-none`}
/>
{formTouched.message && formErrors.message && (
<p id="message-error" className="text-sm text-danger flex items-center gap-1">
<AlertCircle className="h-4 w-4" />
{formErrors.message}
</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full inline-flex items-center justify-center gap-2 rounded-md bg-primary px-4 py-3 text-sm font-semibold text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<Spinner size="sm" />
Sending...
</>
) : submitStatus === "success" ? (
<>
<CheckCircle className="h-4 w-4" />
Sent!
</>
) : (
"Submit"
)}
</button>
</form>
<div className="flex flex-col gap-3">
<div className="inline-flex items-center gap-2 text-primary font-semibold">
<MessageSquare className="h-5 w-5" />
<span>By Chat (Anytime)</span>
</div>
<p className="text-sm text-foreground">
Click the bottom right &ldquo;Chat Button&rdquo; to reach our team anytime.
</p>
<div className="inline-flex items-center gap-2 text-primary font-semibold">
<PhoneCall className="h-5 w-5" />
<span>By Phone (9:30-18:00 JST)</span>
</div>
<div className="text-sm text-foreground">
<p className="font-semibold">Toll Free within Japan</p>
<p className="text-lg font-bold text-primary">0120-660-470</p>
<p className="font-semibold mt-1">From Overseas (may incur calling rates)</p>
<p className="text-lg font-bold text-primary">+81-3-3560-1006</p>
</div>
</div>
</div>
<div className="space-y-6">
<div className="w-full rounded-2xl overflow-hidden shadow-md border border-border/60 bg-card aspect-[4/3]">
<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-full"
loading="lazy"
allowFullScreen
referrerPolicy="no-referrer-when-downgrade"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="rounded-2xl bg-card shadow-sm border border-border/60 p-5 space-y-2">
<div className="inline-flex items-center gap-2 text-primary font-semibold">
<Train className="h-5 w-5" />
<span>Access</span>
</div>
<p className="text-sm text-foreground">
Subway Oedo Line / Nanboku Line
<br />
Short distance walk from exit 6 of Azabu-Juban Station
<br />
(1F of our building is Domino&apos;s Pizza)
</p>
</div>
<div className="rounded-2xl bg-card shadow-sm border border-border/60 p-5 space-y-2">
<div className="inline-flex items-center gap-2 text-primary font-semibold">
<MapPin className="h-5 w-5" />
<span>Address</span>
</div>
<p className="text-sm text-foreground">
3F Azabu Maruka Bldg.,
<br />
3-8-2 Higashi Azabu, Minato-ku,
<br />
Tokyo 106-0044
<br />
Tel: 03-3560-1006 Fax: 03-3560-1007
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -1,14 +1,7 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { ArrowRight } from "lucide-react";
import {
personalServices,
businessServices,
mobileQuickServices,
type ServiceCategory,
} from "@/features/landing-page/data";
import { useInView } from "@/features/landing-page/hooks";
interface HeroSectionProps {
@ -17,12 +10,11 @@ interface HeroSectionProps {
export function HeroSection({ heroCTARef }: HeroSectionProps) {
const [heroRef, heroInView] = useInView();
const [activeCategory, setActiveCategory] = useState<ServiceCategory>("personal");
return (
<section
ref={heroRef as React.RefObject<HTMLElement>}
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-12 sm:py-16 overflow-hidden transition-all duration-700 ${
className={`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 ${
heroInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
>
@ -46,141 +38,34 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
}}
/>
<div className="relative mx-auto max-w-6xl grid grid-cols-1 lg:grid-cols-[1.05fr_minmax(0,0.95fr)] items-center gap-10 lg:gap-16 px-6 sm:px-10 lg:px-14">
<div className="space-y-6 text-left max-w-2xl">
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold leading-tight text-foreground">
<span className="block whitespace-nowrap">A One Stop Solution</span>
<span className="block text-primary mt-2 whitespace-nowrap">for Your IT Needs</span>
</h1>
<p className="text-base sm:text-lg text-muted-foreground leading-relaxed font-semibold">
No Japanese required. Get reliable internet, mobile, and VPN services with full English
support. Serving expats and international businesses for over 20 years.
</p>
<div
ref={heroCTARef}
className="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 pt-2"
<div className="relative mx-auto max-w-3xl px-6 sm:px-10 lg:px-14 text-center">
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold leading-tight text-foreground">
<span className="block">English IT Support</span>
<span className="block text-primary mt-2">for Expats in Japan</span>
</h1>
<p className="mt-6 text-base sm:text-lg text-muted-foreground leading-relaxed font-semibold max-w-2xl mx-auto">
No Japanese required. Get reliable internet, mobile, and VPN services with full English
support. Serving expats and international businesses for over 20 years.
</p>
<div
ref={heroCTARef}
className="mt-8 flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4"
>
<Link
href="/services"
className="inline-flex items-center justify-center gap-2 rounded-full px-6 sm:px-8 py-3 text-base font-semibold bg-primary text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 transition-transform hover:-translate-y-0.5"
>
<Link
href="/services"
className="inline-flex items-center justify-center gap-2 rounded-full px-6 sm:px-8 py-3 text-base font-semibold bg-primary text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 transition-transform hover:-translate-y-0.5"
>
Browse Services
<ArrowRight className="h-5 w-5" />
</Link>
<Link
href="/contact"
className="inline-flex items-center justify-center gap-2 rounded-full px-6 sm:px-8 py-3 text-base font-semibold border border-border bg-card text-primary hover:bg-primary/5 transition-colors"
>
Need Assistance?
</Link>
</div>
</div>
{/* Mobile Quick Services - visible on mobile only */}
<div className="lg:hidden w-full">
<div
className="flex gap-3 overflow-x-auto pb-4 snap-x snap-mandatory"
style={{ scrollbarWidth: "none" }}
Browse Services
<ArrowRight className="h-5 w-5" />
</Link>
<Link
href="/contact"
className="inline-flex items-center justify-center gap-2 rounded-full px-6 sm:px-8 py-3 text-base font-semibold border border-border bg-card text-primary hover:bg-primary/5 transition-colors"
>
{mobileQuickServices.map(service => (
<Link
key={service.title}
href={service.href}
className="flex-shrink-0 w-[140px] snap-start group"
>
<div className="flex flex-col items-center gap-2 p-4 rounded-xl border border-border/70 bg-card shadow-sm hover:shadow-md transition-shadow">
<div className="w-12 h-12 rounded-full border-2 border-primary/30 flex items-center justify-center group-hover:border-primary/60 transition-colors">
<div className="[&>svg]:h-6 [&>svg]:w-6">{service.icon}</div>
</div>
<span className="text-xs font-semibold text-foreground text-center">
{service.title}
</span>
</div>
</Link>
))}
<Link href="/services" className="flex-shrink-0 w-[140px] snap-start group">
<div className="flex flex-col items-center justify-center gap-2 p-4 rounded-xl border border-dashed border-primary/40 bg-primary/5 h-full min-h-[120px] hover:bg-primary/10 transition-colors">
<ArrowRight className="h-6 w-6 text-primary" />
<span className="text-xs font-semibold text-primary text-center">
See All Services
</span>
</div>
</Link>
</div>
</div>
{/* Desktop Services Panel - hidden on mobile */}
<div className="hidden lg:block relative w-full">
<div className="rounded-2xl border border-border/70 bg-card shadow-lg p-6">
{/* Tab Switcher */}
<div className="flex mb-6 bg-muted rounded-full p-1">
<button
type="button"
onClick={() => setActiveCategory("personal")}
className={`flex-1 py-2.5 px-4 text-sm font-semibold rounded-full transition-all ${
activeCategory === "personal"
? "bg-foreground text-background shadow-md"
: "text-muted-foreground hover:text-foreground"
}`}
>
Personal Services
</button>
<button
type="button"
onClick={() => setActiveCategory("business")}
className={`flex-1 py-2.5 px-4 text-sm font-semibold rounded-full transition-all ${
activeCategory === "business"
? "bg-foreground text-background shadow-md"
: "text-muted-foreground hover:text-foreground"
}`}
>
Business Services
</button>
</div>
{/* Services Grid */}
<div className="grid grid-cols-2 gap-3 h-[320px] content-start overflow-hidden">
{(activeCategory === "personal" ? personalServices : businessServices).map(
service => (
<Link
key={service.title}
href={service.href}
className={`group flex flex-col items-center justify-center gap-2 p-3 rounded-xl hover:bg-muted/50 transition-colors ${
activeCategory === "personal" ? "h-[152px]" : "h-[100px]"
}`}
>
<div
className={`rounded-full border-2 border-primary/30 flex items-center justify-center group-hover:border-primary/60 transition-colors flex-shrink-0 ${
activeCategory === "personal" ? "w-20 h-20" : "w-14 h-14"
}`}
>
<div
className={
activeCategory === "personal"
? "[&>svg]:h-10 [&>svg]:w-10"
: "[&>svg]:h-7 [&>svg]:w-7"
}
>
{service.icon}
</div>
</div>
<span
className={`font-semibold text-foreground text-center leading-tight ${
activeCategory === "personal" ? "text-sm" : "text-xs"
}`}
>
{service.title}
</span>
</Link>
)
)}
</div>
</div>
Need Assistance?
</Link>
</div>
</div>
{/* Gradient fade to next section */}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-background pointer-events-none" />
</section>
);
}

View File

@ -1,122 +0,0 @@
"use client";
import Link from "next/link";
import { ArrowRight, Check, Wifi, Smartphone } from "lucide-react";
import { useInView } from "@/features/landing-page/hooks";
export function PopularServicesSection() {
const [popularRef, popularInView] = useInView();
return (
<section
ref={popularRef as React.RefObject<HTMLElement>}
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-background py-14 sm:py-16 transition-all duration-700 ${
popularInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
>
<div className="max-w-6xl mx-auto px-6 sm:px-10 lg:px-14">
{/* Section Header */}
<div className="text-center mb-10 sm:mb-12">
<h2 className="text-3xl sm:text-4xl font-extrabold text-foreground mb-4">
Most Popular Services
</h2>
<p className="text-base sm:text-lg text-muted-foreground max-w-2xl mx-auto">
Get connected with ease. No Japanese required, no complicated paperwork, and full
English support every step of the way.
</p>
</div>
{/* Two-column card layout */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
{/* Internet Plans Card */}
<div className="relative rounded-2xl border border-info-border bg-card shadow-lg p-6 sm:p-8 flex flex-col">
<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-primary-foreground uppercase tracking-wide">
Popular
</span>
</div>
<div className="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-info-soft flex items-center justify-center mb-5">
<Wifi className="h-8 w-8 sm:h-10 sm:w-10 text-primary" />
</div>
<h3 className="text-xl sm:text-2xl font-bold text-foreground mb-3">Internet Plans</h3>
<p className="text-muted-foreground mb-5 leading-relaxed">
High-speed fiber internet on Japan&apos;s reliable NTT network. We handle all the
Japanese paperwork and coordinate installation in English.
</p>
<ul className="space-y-3 mb-6 flex-grow">
{[
"Speeds up to 10 Gbps available",
"English installation coordination",
"No Japanese contracts to sign",
"Foreign credit cards accepted",
].map(feature => (
<li key={feature} className="flex items-start gap-3">
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center mt-0.5">
<Check className="h-3 w-3 text-primary" />
</span>
<span className="text-sm text-foreground">{feature}</span>
</li>
))}
</ul>
<Link
href="/services/internet"
className="w-full inline-flex items-center justify-center gap-2 rounded-full bg-primary px-6 py-3.5 text-base font-semibold text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 transition-all hover:-translate-y-0.5"
>
View Plans
<ArrowRight className="h-5 w-5" />
</Link>
</div>
{/* Phone Plans Card */}
<div className="relative rounded-2xl border border-info-border bg-card shadow-lg p-6 sm:p-8 flex flex-col">
<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-primary-foreground uppercase tracking-wide">
Popular
</span>
</div>
<div className="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-info-soft flex items-center justify-center mb-5">
<Smartphone className="h-8 w-8 sm:h-10 sm:w-10 text-primary" />
</div>
<h3 className="text-xl sm:text-2xl font-bold text-foreground mb-3">Phone Plans</h3>
<p className="text-muted-foreground mb-5 leading-relaxed">
Mobile SIM cards with voice and data on Japan&apos;s top network. Sign up online, no
hanko needed, and get your SIM delivered fast.
</p>
<ul className="space-y-3 mb-6 flex-grow">
{[
"Data-only and voice + data options",
"Keep your number with MNP transfer",
"eSIM available for instant activation",
"Flexible plans with no long-term contracts",
].map(feature => (
<li key={feature} className="flex items-start gap-3">
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center mt-0.5">
<Check className="h-3 w-3 text-primary" />
</span>
<span className="text-sm text-foreground">{feature}</span>
</li>
))}
</ul>
<Link
href="/services/sim"
className="w-full inline-flex items-center justify-center gap-2 rounded-full bg-primary px-6 py-3.5 text-base font-semibold text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 transition-all hover:-translate-y-0.5"
>
View Plans
<ArrowRight className="h-5 w-5" />
</Link>
</div>
</div>
</div>
</section>
);
}

View File

@ -1,192 +0,0 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { ArrowRight } from "lucide-react";
import { supportDownloads } from "@/features/landing-page/data";
import { useInView } from "@/features/landing-page/hooks";
export function RemoteSupportSection() {
const [supportRef, supportInView] = useInView();
const [remoteSupportTab, setRemoteSupportTab] = useState(0);
return (
<section
ref={supportRef as React.RefObject<HTMLElement>}
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-background py-12 sm:py-16 transition-all duration-700 overflow-hidden ${
supportInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
>
{/* Gradient fade to next section */}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-primary-soft pointer-events-none" />
<div className="relative max-w-6xl mx-auto px-6 sm:px-10 lg:px-14">
{/* Section header */}
<div className="flex items-center gap-4 mb-8">
<div className="relative">
<h2 className="text-3xl sm:text-4xl font-extrabold text-primary tracking-tight">
Remote Support
</h2>
<p className="text-sm sm:text-base text-muted-foreground mt-2 max-w-md">
Download one of our secure tools and let our technicians help you remotely
</p>
</div>
</div>
{/* Mobile: Stacked Cards Layout */}
<div className="md:hidden space-y-4">
{supportDownloads.map(item => (
<div
key={item.title}
className="group relative bg-card rounded-2xl border border-border/60 shadow-sm hover:shadow-lg transition-all duration-300 overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-sky-400 to-cyan-500" />
<div className="p-5">
<div className="flex items-start gap-4">
<div className="relative flex-shrink-0">
<div className="w-16 h-16 rounded-xl flex items-center justify-center bg-gradient-to-br from-sky-50 to-cyan-100">
<Image
src={item.image}
alt={item.title}
width={48}
height={48}
className="object-contain w-10 h-10"
/>
</div>
<span className="absolute -top-1 -right-1 w-3 h-3 rounded-full bg-sky-400 animate-pulse" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-foreground mb-1">{item.title}</h3>
<p className="text-xs font-semibold uppercase tracking-wide mb-2 text-info">
{item.useCase.replace("Best for: ", "")}
</p>
<p className="text-sm text-muted-foreground leading-relaxed line-clamp-2">
{item.description}
</p>
</div>
</div>
<Link
href={item.href}
target="_blank"
rel="noopener noreferrer"
className="mt-4 flex items-center justify-center gap-2 w-full rounded-xl py-3.5 text-sm font-semibold text-white transition-all duration-200 active:scale-[0.98] bg-gradient-to-r from-sky-500 to-cyan-600 hover:from-sky-600 hover:to-cyan-700"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
Download {item.title.split(" ")[0]}
</Link>
</div>
</div>
))}
</div>
{/* Desktop: Tabbed Layout */}
<div className="hidden md:block">
<div className="bg-card rounded-3xl border border-border/60 shadow-lg overflow-hidden">
<div className="flex border-b border-border/40">
{supportDownloads.map((item, index) => (
<button
key={item.title}
type="button"
onClick={() => setRemoteSupportTab(index)}
className={`flex-1 flex items-center justify-center gap-3 py-4 px-6 text-base font-medium transition-all duration-200 relative ${
remoteSupportTab === index
? "text-primary bg-primary/5"
: "text-muted-foreground hover:text-foreground hover:bg-muted/30"
}`}
aria-pressed={remoteSupportTab === index}
>
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors ${
remoteSupportTab === index ? "bg-info-soft" : "bg-muted/50"
}`}
>
<Image
src={item.image}
alt=""
width={28}
height={28}
className="object-contain"
/>
</div>
<span>{item.title}</span>
{remoteSupportTab === index && (
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-to-r from-sky-400 to-cyan-500" />
)}
</button>
))}
</div>
{(() => {
const currentDownload = supportDownloads[remoteSupportTab];
if (!currentDownload) return null;
return (
<div className="p-8 lg:p-12">
<div className="flex items-start gap-8 lg:gap-12">
<div className="h-32 w-32 lg:h-40 lg:w-40 rounded-2xl flex items-center justify-center flex-shrink-0 transition-colors bg-gradient-to-br from-sky-50 to-cyan-100">
<Image
src={currentDownload.image}
alt={currentDownload.title}
width={96}
height={96}
className="object-contain w-20 h-20 lg:w-24 lg:h-24"
/>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold uppercase tracking-wide mb-2 text-info">
{currentDownload.useCase}
</p>
<h3 className="text-2xl lg:text-3xl font-bold text-foreground mb-4">
{currentDownload.title}
</h3>
<p className="text-base lg:text-lg text-muted-foreground leading-relaxed mb-6">
{currentDownload.description}
</p>
<Link
href={currentDownload.href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-full px-8 py-3.5 text-base font-semibold text-white transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg bg-gradient-to-r from-sky-500 to-cyan-600 hover:from-sky-600 hover:to-cyan-700 shadow-sky-200"
>
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
Download Now
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div>
</div>
);
})()}
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,41 @@
"use client";
import { ServiceCard } from "@/components/molecules";
import { landingServices } from "@/features/landing-page/data";
import { useInView } from "@/features/landing-page/hooks";
export function ServicesGrid() {
const [ref, isInView] = useInView();
return (
<section
ref={ref as React.RefObject<HTMLElement>}
className={`py-14 sm:py-16 transition-all duration-700 ${
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
>
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-14">
<div className="text-center mb-10">
<h2 className="text-2xl sm:text-3xl font-extrabold text-foreground">Our Services</h2>
<p className="mt-2 text-base text-muted-foreground">
Everything you need to stay connected in Japan
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
{landingServices.map(service => (
<ServiceCard
key={service.title}
variant="default"
href={service.href}
icon={service.icon}
title={service.title}
description={service.description}
accentColor={service.accentColor}
/>
))}
</div>
</div>
</section>
);
}

View File

@ -1,281 +0,0 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { services } from "@/features/landing-page/data";
import { useInView } from "@/features/landing-page/hooks";
export function SolutionsCarousel() {
const [solutionsRef, solutionsInView] = useInView();
const carouselRef = useRef<HTMLDivElement>(null);
const itemWidthRef = useRef(0);
const [currentSlide, setCurrentSlide] = useState(0);
const isScrollingRef = useRef(false);
const autoScrollTimerRef = useRef<NodeJS.Timeout | null>(null);
const touchStartXRef = useRef(0);
const touchEndXRef = useRef(0);
const [pageCount, setPageCount] = useState(services.length);
const computeItemWidth = useCallback(() => {
const container = carouselRef.current;
if (!container) return;
const card = container.querySelector<HTMLElement>("[data-service-card]");
if (!card) return;
const gap =
Number.parseFloat(getComputedStyle(container).columnGap || "0") ||
Number.parseFloat(getComputedStyle(container).gap || "0") ||
24;
itemWidthRef.current = card.getBoundingClientRect().width + gap;
// Calculate page count based on how many cards are visible at once
const containerWidth = container.clientWidth;
const visibleCards = Math.max(1, Math.floor(containerWidth / itemWidthRef.current));
setPageCount(Math.max(1, services.length - visibleCards + 1));
}, []);
const scrollByOne = useCallback((direction: 1 | -1) => {
const container = carouselRef.current;
if (!container || !itemWidthRef.current || isScrollingRef.current) return;
isScrollingRef.current = true;
container.scrollBy({
left: direction * itemWidthRef.current,
behavior: "smooth",
});
setTimeout(() => {
isScrollingRef.current = false;
}, 500);
}, []);
const scrollToIndex = useCallback((index: number) => {
const container = carouselRef.current;
if (!container || !itemWidthRef.current) return;
container.scrollTo({
left: index * itemWidthRef.current,
behavior: "smooth",
});
}, []);
const updateCurrentSlide = useCallback(() => {
const container = carouselRef.current;
if (!container || !itemWidthRef.current) return;
const scrollLeft = container.scrollLeft;
const newIndex = Math.round(scrollLeft / itemWidthRef.current);
setCurrentSlide(Math.max(0, Math.min(newIndex, services.length - 1)));
}, []);
const startAutoScroll = useCallback(() => {
if (autoScrollTimerRef.current) {
clearInterval(autoScrollTimerRef.current);
}
autoScrollTimerRef.current = setInterval(() => {
scrollByOne(1);
}, 5000);
}, [scrollByOne]);
const stopAutoScroll = useCallback(() => {
if (autoScrollTimerRef.current) {
clearInterval(autoScrollTimerRef.current);
autoScrollTimerRef.current = null;
}
}, []);
const handleTouchStart = useCallback((e: React.TouchEvent) => {
const touch = e.touches[0];
if (touch) {
touchStartXRef.current = touch.clientX;
touchEndXRef.current = touch.clientX;
}
}, []);
const handleTouchMove = useCallback((e: React.TouchEvent) => {
const touch = e.touches[0];
if (touch) {
touchEndXRef.current = touch.clientX;
}
}, []);
const handleTouchEnd = useCallback(() => {
const diff = touchStartXRef.current - touchEndXRef.current;
const minSwipeDistance = 50;
if (Math.abs(diff) > minSwipeDistance) {
if (diff > 0) {
scrollByOne(1);
} else {
scrollByOne(-1);
}
}
startAutoScroll();
}, [scrollByOne, startAutoScroll]);
useEffect(() => {
computeItemWidth();
const handleResize = () => computeItemWidth();
window.addEventListener("resize", handleResize);
startAutoScroll();
return () => {
window.removeEventListener("resize", handleResize);
stopAutoScroll();
};
}, [computeItemWidth, startAutoScroll, stopAutoScroll]);
useEffect(() => {
const container = carouselRef.current;
if (!container) return;
const handleScroll = () => {
updateCurrentSlide();
};
container.addEventListener("scroll", handleScroll, { passive: true });
return () => container.removeEventListener("scroll", handleScroll);
}, [updateCurrentSlide]);
const handleMouseEnter = useCallback(() => {
stopAutoScroll();
}, [stopAutoScroll]);
const handleMouseLeave = useCallback(() => {
startAutoScroll();
}, [startAutoScroll]);
const handlePrevClick = useCallback(() => {
scrollByOne(-1);
startAutoScroll();
}, [scrollByOne, startAutoScroll]);
const handleNextClick = useCallback(() => {
scrollByOne(1);
startAutoScroll();
}, [scrollByOne, startAutoScroll]);
return (
<section
ref={solutionsRef as React.RefObject<HTMLElement>}
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-soft py-12 sm:py-14 transition-all duration-700 ${
solutionsInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
>
{/* Gradient fade to next section */}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-background pointer-events-none" />
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-14">
{/* Header row with title + nav arrows */}
<div className="flex items-center justify-between mb-8">
<h2 className="text-3xl sm:text-4xl font-extrabold text-foreground">Solutions</h2>
{/* Desktop navigation arrows — inline with the heading */}
<div className="hidden sm:flex items-center gap-2">
<button
type="button"
aria-label="Previous solution"
onClick={handlePrevClick}
className="h-10 w-10 rounded-full border border-border/80 bg-card text-foreground shadow-sm hover:bg-muted transition-colors active:scale-95 flex items-center justify-center"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
type="button"
aria-label="Next solution"
onClick={handleNextClick}
className="h-10 w-10 rounded-full border border-border/80 bg-card text-foreground shadow-sm hover:bg-muted transition-colors active:scale-95 flex items-center justify-center"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
</div>
{/* Carousel track */}
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<div
ref={carouselRef}
className="flex gap-6 overflow-x-auto scroll-smooth snap-x snap-mandatory"
style={{ scrollbarWidth: "none", WebkitOverflowScrolling: "touch" }}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{services.map((service, index) => (
<Link
key={`${service.title}-${index}`}
href={service.href}
className="group flex-shrink-0 w-[280px] sm:w-[300px] snap-start"
>
<article
data-service-card
className="h-full rounded-2xl bg-card px-6 py-8 shadow-md border border-border/60 transition-all duration-300 group-hover:-translate-y-1 group-hover:shadow-lg flex flex-col"
>
<div className="mb-5 w-14 h-14 rounded-xl bg-primary/8 flex items-center justify-center">
{service.icon}
</div>
<h3 className="text-lg font-bold text-foreground mb-2 font-display">
{service.title}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed flex-grow">
{service.description}
</p>
</article>
</Link>
))}
</div>
</div>
{/* Progress dots + mobile arrows */}
<div className="flex items-center justify-center gap-4 mt-6">
{/* Mobile-only prev arrow */}
<button
type="button"
aria-label="Previous solution"
onClick={handlePrevClick}
className="sm:hidden h-9 w-9 rounded-full border border-border/80 bg-card text-foreground shadow-sm hover:bg-muted transition-colors active:scale-95 flex items-center justify-center flex-shrink-0"
>
<ChevronLeft className="h-4 w-4" />
</button>
{/* Dots — one per scroll page, not per card */}
<div className="flex items-center gap-2">
{(() => {
const activeDot = Math.min(currentSlide, pageCount - 1);
return Array.from({ length: pageCount }).map((_, index) => (
<button
key={index}
className={`h-2 rounded-full transition-all duration-300 ${
activeDot === index
? "bg-primary w-8"
: "bg-foreground/20 w-2 hover:bg-foreground/40"
}`}
onClick={() => scrollToIndex(index)}
aria-label={`Go to page ${index + 1}`}
/>
));
})()}
</div>
{/* Mobile-only next arrow */}
<button
type="button"
aria-label="Next solution"
onClick={handleNextClick}
className="sm:hidden h-9 w-9 rounded-full border border-border/80 bg-card text-foreground shadow-sm hover:bg-muted transition-colors active:scale-95 flex items-center justify-center flex-shrink-0"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,25 @@
import { Clock, CreditCard, Globe, Users } from "lucide-react";
const trustItems = [
{ icon: Clock, label: "20+ Years" },
{ icon: Globe, label: "Full English" },
{ icon: CreditCard, label: "Foreign Cards" },
{ icon: Users, label: "10,000+ Customers" },
];
export function TrustStrip() {
return (
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 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">
{trustItems.map(({ icon: Icon, label }) => (
<div key={label} className="flex items-center gap-2.5">
<Icon className="h-5 w-5 text-primary shrink-0" />
<span className="text-sm font-semibold text-foreground">{label}</span>
</div>
))}
</div>
</div>
</section>
);
}

View File

@ -5,29 +5,31 @@ import Link from "next/link";
import { ArrowRight, BadgeCheck } from "lucide-react";
import { useInView } from "@/features/landing-page/hooks";
export function TrustSection() {
const [trustRef, trustInView] = useInView();
const trustPoints = [
"Full English support, no Japanese needed",
"Foreign credit cards accepted",
"Bilingual contracts and documentation",
];
export function WhyUsSection() {
const [ref, isInView] = useInView();
return (
<section
ref={trustRef as React.RefObject<HTMLElement>}
ref={ref as React.RefObject<HTMLElement>}
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-background transition-all duration-700 ${
trustInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
>
{/* Gradient fade to next section */}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-primary-soft pointer-events-none" />
<div className="max-w-6xl mx-auto px-6 sm:px-10 lg:px-14 py-14 sm:py-16">
<div className="grid grid-cols-1 lg:grid-cols-[1fr_1.1fr] gap-10 lg:gap-14 items-center">
<div className="relative w-full overflow-hidden rounded-2xl shadow-md border border-border/60 bg-card aspect-[4/5]">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-14 items-center">
<div className="relative w-full overflow-hidden rounded-2xl shadow-md border border-border/60 bg-card aspect-[3/2]">
<Image
src="/assets/images/Why_us.png"
alt="Team collaborating with trust and excellence"
fill
priority
className="object-cover"
sizes="(max-width: 1024px) 100vw, 40vw"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
</div>
<div className="space-y-6">
@ -42,11 +44,7 @@ export function TrustSection() {
</div>
<ul className="space-y-3 text-foreground">
{[
"Full English support, no Japanese needed",
"Foreign credit cards accepted",
"Bilingual contracts and documentation",
].map(item => (
{trustPoints.map(item => (
<li key={item} className="flex items-center gap-3 text-base font-semibold">
<BadgeCheck className="h-5 w-5 text-primary" />
<span>{item}</span>

View File

@ -1,10 +1,9 @@
// Landing page sections
export { HeroSection } from "./HeroSection";
export { TrustSection } from "./TrustSection";
export { SolutionsCarousel } from "./SolutionsCarousel";
export { PopularServicesSection } from "./PopularServicesSection";
export { RemoteSupportSection } from "./RemoteSupportSection";
export { ContactSection } from "./ContactSection";
export { TrustStrip } from "./TrustStrip";
export { ServicesGrid } from "./ServicesGrid";
export { WhyUsSection } from "./WhyUsSection";
export { CTABanner } from "./CTABanner";
// Trust indicators
export { TrustBadge } from "./TrustBadge";
@ -14,7 +13,3 @@ export { TrustIndicators } from "./TrustIndicators";
export { ValuePropCard } from "./ValuePropCard";
export { FloatingGlassCard } from "./FloatingGlassCard";
export { AnimatedBackground } from "./AnimatedBackground";
// NOTE: ServiceCard components have been consolidated into @/components/molecules/ServiceCard
// Use: import { ServiceCard } from "@/components/molecules/ServiceCard"
// The unified ServiceCard supports variants: 'default' | 'featured' | 'minimal' | 'bento-sm' | 'bento-md' | 'bento-lg'

View File

@ -1,11 +1,13 @@
export {
type ServiceCategory,
type ServiceItem,
type LandingServiceItem,
personalServices,
businessServices,
services,
supportDownloads,
mobileQuickServices,
landingServices,
} from "./services";
export {

View File

@ -9,6 +9,7 @@ import {
Wifi,
Wrench,
} from "lucide-react";
import type { ServiceCardAccentColor } from "@/components/molecules";
// =============================================================================
// TYPES
@ -137,3 +138,50 @@ export const supportDownloads = [
// Mobile quick services (top 3 most popular)
export const mobileQuickServices = personalServices.slice(0, 3);
// =============================================================================
// LANDING PAGE SERVICES (for ServicesGrid)
// =============================================================================
export interface LandingServiceItem {
title: string;
description: string;
icon: React.ReactNode;
href: string;
accentColor: ServiceCardAccentColor;
}
export const landingServices: LandingServiceItem[] = [
{
title: "Internet Plans",
description:
"High-speed NTT fiber with English installation support. No Japanese paperwork needed.",
icon: <Wifi className="h-6 w-6" />,
href: "/services/internet",
accentColor: "blue",
},
{
title: "Phone Plans",
description:
"SIM cards on Japan's best network. Foreign credit cards accepted, no hanko required.",
icon: <Smartphone className="h-6 w-6" />,
href: "/services/sim",
accentColor: "green",
},
{
title: "VPN Service",
description:
"Stream your favorite shows from home. Pre-configured router, just plug in and watch.",
icon: <Lock className="h-6 w-6" />,
href: "/services/vpn",
accentColor: "purple",
},
{
title: "Business Solutions",
description:
"Enterprise IT for international companies. Dedicated internet, networks, and bilingual support.",
icon: <Building2 className="h-6 w-6" />,
href: "/services/business",
accentColor: "orange",
},
];

View File

@ -1,2 +1,3 @@
export { useInView } from "./useInView";
export { useContactForm } from "./useContactForm";
export { useStickyCta } from "./useStickyCta";

View File

@ -0,0 +1,33 @@
"use client";
import { useEffect, useRef, useState } from "react";
/**
* useStickyCta - Tracks hero CTA visibility for sticky mobile CTA.
*
* Uses IntersectionObserver to detect when the hero CTA scrolls out of view,
* returning a boolean to control showing/hiding the sticky bottom CTA.
*/
export function useStickyCta() {
const heroCTARef = useRef<HTMLDivElement>(null);
const [showStickyCTA, setShowStickyCTA] = useState(false);
useEffect(() => {
const ctaElement = heroCTARef.current;
if (!ctaElement) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry) {
setShowStickyCTA(!entry.isIntersecting);
}
},
{ threshold: 0 }
);
observer.observe(ctaElement);
return () => observer.disconnect();
}, []);
return { heroCTARef, showStickyCTA };
}

View File

@ -2,52 +2,62 @@ import { Skeleton } from "@/components/atoms/loading-skeleton";
export function PublicLandingLoadingView() {
return (
<div className="space-y-0 pb-8 pt-0 sm:pt-0">
<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-12 sm:py-16">
<div className="mx-auto max-w-6xl grid grid-cols-1 lg:grid-cols-[1.05fr_minmax(0,0.95fr)] items-center gap-10 lg:gap-16 px-6 sm:px-10 lg:px-14 pt-0">
<div className="space-y-4 max-w-2xl">
<div className="space-y-2">
<Skeleton className="h-10 w-72 max-w-full rounded-md" />
<Skeleton className="h-10 w-80 max-w-full rounded-md" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-full rounded-md" />
<Skeleton className="h-4 w-4/5 rounded-md" />
</div>
<div className="flex flex-col sm:flex-row gap-3 pt-2">
<Skeleton className="h-12 w-48 rounded-full" />
<Skeleton className="h-12 w-48 rounded-full" />
</div>
<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">
<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" />
<Skeleton className="h-10 w-56 mx-auto rounded-md" />
</div>
<div className="w-full">
<Skeleton className="w-full aspect-[4/3] rounded-2xl" />
<div className="space-y-2 max-w-2xl mx-auto">
<Skeleton className="h-4 w-full rounded-md" />
<Skeleton className="h-4 w-4/5 mx-auto rounded-md" />
</div>
<div className="flex justify-center gap-3 pt-2">
<Skeleton className="h-12 w-48 rounded-full" />
<Skeleton className="h-12 w-48 rounded-full" />
</div>
</div>
</section>
{/* Solutions Carousel Skeleton */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-soft py-12 sm:py-14">
<div className="mx-auto max-w-7xl px-6 sm:px-10 lg:px-14 space-y-6">
<Skeleton className="h-10 w-40 rounded-md" />
<div className="flex gap-6 overflow-hidden">
{/* 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">
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-14 py-5">
<div className="flex justify-between">
{Array.from({ length: 4 }).map((_, idx) => (
<div key={idx} className="w-[260px] flex-shrink-0">
<Skeleton className="h-64 w-full rounded-3xl" />
<div key={idx} className="flex items-center gap-2.5">
<Skeleton className="h-5 w-5 rounded-full" />
<Skeleton className="h-4 w-24 rounded-md" />
</div>
))}
</div>
</div>
</section>
{/* Trust and Excellence Skeleton */}
{/* ServicesGrid Skeleton */}
<section className="py-14 sm:py-16">
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-14">
<div className="text-center mb-10 space-y-2">
<Skeleton className="h-8 w-40 mx-auto rounded-md" />
<Skeleton className="h-4 w-64 mx-auto rounded-md" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
{Array.from({ length: 4 }).map((_, idx) => (
<Skeleton key={idx} className="h-52 w-full rounded-2xl" />
))}
</div>
</div>
</section>
{/* WhyUsSection Skeleton */}
<section className="max-w-6xl mx-auto px-6 sm:px-10 lg:px-14 py-14 sm:py-16">
<div className="grid grid-cols-1 lg:grid-cols-[1fr_1.1fr] gap-10 lg:gap-14 items-center">
<Skeleton className="h-full w-full rounded-2xl min-h-[320px]" />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-14 items-center">
<Skeleton className="w-full aspect-[3/2] rounded-2xl" />
<div className="space-y-4">
<Skeleton className="h-4 w-36 rounded-md" />
<Skeleton className="h-10 w-3/4 rounded-md" />
<Skeleton className="h-10 w-1/2 rounded-md" />
<Skeleton className="h-8 w-3/4 rounded-md" />
<Skeleton className="h-6 w-full rounded-md" />
<Skeleton className="h-6 w-4/5 rounded-md" />
<div className="space-y-3 pt-2">
{Array.from({ length: 3 }).map((_, idx) => (
<div key={idx} className="flex items-center gap-3">
@ -61,46 +71,15 @@ export function PublicLandingLoadingView() {
</div>
</section>
{/* Support Downloads Skeleton */}
<section className="max-w-5xl mx-auto px-6 sm:px-10 lg:px-14 pb-16">
<Skeleton className="h-10 w-40 mx-auto rounded-md mb-10" />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8 sm:gap-10">
{Array.from({ length: 2 }).map((_, idx) => (
<div
key={idx}
className="rounded-2xl border border-border/60 bg-card p-6 shadow-sm space-y-4"
>
<Skeleton className="h-5 w-32 mx-auto rounded-md" />
<Skeleton className="h-24 w-24 mx-auto rounded-full" />
<Skeleton className="h-9 w-32 mx-auto rounded-md" />
</div>
))}
</div>
</section>
{/* Contact Skeleton */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-primary-soft py-14 sm:py-16">
<div className="max-w-6xl mx-auto px-6 sm:px-10 lg:px-14 space-y-6">
<Skeleton className="h-10 w-48 rounded-md" />
<div className="grid grid-cols-1 lg:grid-cols-[1.1fr_0.9fr] gap-10 lg:gap-12">
<div className="rounded-2xl bg-card shadow-sm border border-border/60 p-6 sm:p-8 space-y-4">
<Skeleton className="h-8 w-48 rounded-md" />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Skeleton className="h-11 w-full rounded-md" />
<Skeleton className="h-11 w-full rounded-md" />
</div>
<Skeleton className="h-11 w-full rounded-md" />
<Skeleton className="h-11 w-full rounded-md" />
<Skeleton className="h-11 w-full rounded-md" />
<Skeleton className="h-11 w-full rounded-md" />
<Skeleton className="h-28 w-full rounded-md" />
<Skeleton className="h-11 w-full rounded-md" />
</div>
<div className="space-y-6">
<Skeleton className="h-48 w-full rounded-2xl" />
<Skeleton className="h-28 w-full rounded-2xl" />
<Skeleton className="h-20 w-full rounded-2xl" />
</div>
{/* CTABanner Skeleton */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 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" />
<Skeleton className="h-6 w-36 mx-auto rounded-md" />
<div className="flex justify-center gap-3 pt-2">
<Skeleton className="h-12 w-48 rounded-full" />
<Skeleton className="h-12 w-40 rounded-full" />
</div>
</div>
</section>

View File

@ -2,47 +2,25 @@
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { useContactForm } from "@/features/landing-page/hooks";
import { useStickyCta } from "@/features/landing-page/hooks";
import {
HeroSection,
TrustSection,
SolutionsCarousel,
PopularServicesSection,
RemoteSupportSection,
ContactSection,
TrustStrip,
ServicesGrid,
WhyUsSection,
CTABanner,
} from "@/features/landing-page/components";
export function PublicLandingView() {
const {
heroCTARef,
showStickyCTA,
formData,
formErrors,
formTouched,
isSubmitting,
submitStatus,
handleInputChange,
handleInputBlur,
handleSubmit,
} = useContactForm();
const { heroCTARef, showStickyCTA } = useStickyCta();
return (
<div className="space-y-0 pb-8 pt-0">
<HeroSection heroCTARef={heroCTARef} />
<TrustSection />
<SolutionsCarousel />
<PopularServicesSection />
<RemoteSupportSection />
<ContactSection
formData={formData}
formErrors={formErrors}
formTouched={formTouched}
isSubmitting={isSubmitting}
submitStatus={submitStatus}
handleInputChange={handleInputChange}
handleInputBlur={handleInputBlur}
handleSubmit={handleSubmit}
/>
<TrustStrip />
<ServicesGrid />
<WhyUsSection />
<CTABanner />
{/* Sticky Mobile CTA */}
{showStickyCTA && (

View File

@ -35,7 +35,9 @@ export function HowItWorks({
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-2">
{eyebrow}
</p>
<h2 className="text-display-sm font-display text-foreground">{title}</h2>
<h2 className="text-display-sm font-display font-semibold leading-tight text-foreground">
{title}
</h2>
</div>
{/* Steps Container */}

View File

@ -64,7 +64,9 @@ export function ServiceCTA({
</div>
{/* Headline */}
<h2 className="text-display-sm font-display text-foreground mb-3">{headline}</h2>
<h2 className="text-display-sm font-display font-semibold leading-tight text-foreground mb-3">
{headline}
</h2>
{/* Description */}
<p className="text-base text-muted-foreground mb-6 max-w-md mx-auto">{description}</p>

View File

@ -84,7 +84,9 @@ export function ServiceFAQ({
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-2">
{eyebrow}
</p>
<h2 className="text-display-sm font-display text-foreground">{title}</h2>
<h2 className="text-display-sm font-display font-semibold leading-tight text-foreground">
{title}
</h2>
</div>
{/* FAQ Container */}

View File

@ -57,7 +57,7 @@ export function ServicesHero({
) : null}
<h1
className={cn(
"text-display-xl text-foreground leading-tight font-bold",
"text-display-xl text-foreground leading-display font-extrabold",
displayFont && "font-display",
animationClasses
)}

View File

@ -157,7 +157,7 @@ export function VpnPlansContent({
</span>
</div>
<h1
className="text-display-md md:text-display-lg font-display text-foreground tracking-tight animate-in fade-in slide-in-from-bottom-6 duration-700"
className="text-display-md md:text-display-lg font-display font-semibold md:font-bold leading-tight md:leading-display text-foreground animate-in fade-in slide-in-from-bottom-6 duration-700"
style={{ animationDelay: "100ms" }}
>
Stream Content from Abroad
@ -208,7 +208,9 @@ export function VpnPlansContent({
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-2">
Choose Your Region
</p>
<h2 className="text-display-sm font-display text-foreground">Available Plans</h2>
<h2 className="text-display-sm font-display font-semibold leading-tight text-foreground">
Available Plans
</h2>
<p className="text-sm text-muted-foreground mt-2">
Select one region per router rental
</p>

View File

@ -137,44 +137,32 @@
}
}
/* ===== DISPLAY TYPOGRAPHY (Tailwind v4 @utility) ===== */
/* Only set font-size + letter-spacing. Font weight, line-height, and
font-family are explicit Tailwind classes at the call site so they
can be overridden without cascade conflicts. */
@utility text-display-xl {
font-size: var(--cp-text-display-xl);
letter-spacing: var(--cp-tracking-tight);
}
@utility text-display-lg {
font-size: var(--cp-text-display-lg);
letter-spacing: var(--cp-tracking-tight);
}
@utility text-display-md {
font-size: var(--cp-text-display-md);
letter-spacing: var(--cp-tracking-tight);
}
@utility text-display-sm {
font-size: var(--cp-text-display-sm);
letter-spacing: var(--cp-tracking-tight);
}
@layer utilities {
/* ===== DISPLAY TYPOGRAPHY ===== */
.font-display {
font-family: var(--font-display);
}
.text-display-xl {
font-family: var(--font-display);
font-size: var(--cp-text-display-xl);
font-weight: var(--cp-font-extrabold);
line-height: var(--cp-leading-display);
letter-spacing: var(--cp-tracking-tight);
}
.text-display-lg {
font-family: var(--font-display);
font-size: var(--cp-text-display-lg);
font-weight: var(--cp-font-bold);
line-height: var(--cp-leading-display);
letter-spacing: var(--cp-tracking-tight);
}
.text-display-md {
font-family: var(--font-display);
font-size: var(--cp-text-display-md);
font-weight: var(--cp-font-semibold);
line-height: var(--cp-leading-tight);
letter-spacing: var(--cp-tracking-tight);
}
.text-display-sm {
font-family: var(--font-display);
font-size: var(--cp-text-display-sm);
font-weight: var(--cp-font-semibold);
line-height: var(--cp-leading-tight);
letter-spacing: var(--cp-tracking-tight);
}
/* ===== PAGE ENTRANCE ANIMATIONS ===== */
.cp-animate-in {
animation: cp-fade-up var(--cp-duration-slow) var(--cp-ease-out) forwards;