refactor: update landing page and support components
- Removed the PublicHelpPage component and streamlined navigation by adding a direct link to the Support page in the SiteFooter. - Updated the PublicShell component to redirect to the Support page instead of the Contact page. - Enhanced the CTABanner and HeroSection components with new text and improved call-to-action buttons. - Replaced the ServicesGrid component with ServicesCarousel for better service presentation. - Introduced new conversion service cards in the services data structure for improved service offerings. - Updated the PublicContactView and PublicSupportView components for better styling and accessibility.
This commit is contained in:
parent
5a66adb7e6
commit
ee85426743
@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Public Help Page
|
||||
*
|
||||
* Redirects to the combined Support & Contact page.
|
||||
*/
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function PublicHelpPage() {
|
||||
redirect("/contact");
|
||||
}
|
||||
@ -91,12 +91,20 @@ export function SiteFooter() {
|
||||
About Us
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/support"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Support
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Support & Contact
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@ -256,7 +256,7 @@ export function PublicShell({ children }: PublicShellProps) {
|
||||
Blog
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
href="/support"
|
||||
className="inline-flex items-center px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
||||
>
|
||||
Support
|
||||
@ -390,7 +390,7 @@ export function PublicShell({ children }: PublicShellProps) {
|
||||
Blog
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
href="/support"
|
||||
onClick={closeMobileMenu}
|
||||
className="flex items-center px-3 py-3.5 rounded-xl text-base font-semibold text-foreground hover:bg-muted/50 active:bg-muted/70 transition-colors"
|
||||
>
|
||||
|
||||
@ -1,33 +1,20 @@
|
||||
import { ArrowRight, Phone } from "lucide-react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
|
||||
const PHONE_NUMBER = "03-5812-1050";
|
||||
|
||||
export function CTABanner() {
|
||||
return (
|
||||
<section
|
||||
aria-label="Get in touch"
|
||||
aria-label="Call to action"
|
||||
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 lg:px-14 py-14 sm:py-16 text-center">
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-foreground">
|
||||
Ready to Get Connected?
|
||||
Ready to Get Set Up?
|
||||
</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" aria-hidden="true" />
|
||||
<a
|
||||
href={`tel:${PHONE_NUMBER}`}
|
||||
aria-label={`Call us at ${PHONE_NUMBER}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{PHONE_NUMBER}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||
<Button
|
||||
as="a"
|
||||
@ -36,10 +23,10 @@ export function CTABanner() {
|
||||
size="lg"
|
||||
rightIcon={<ArrowRight className="h-5 w-5" />}
|
||||
>
|
||||
Browse Services
|
||||
Find Your Plan
|
||||
</Button>
|
||||
<Button as="a" href="/contact" variant="pillOutline" size="lg">
|
||||
Contact Us
|
||||
<Button as="a" href="#contact" variant="pillOutline" size="lg">
|
||||
Talk to Us
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { Mail, MapPin, MessageSquare, PhoneCall, Train } from "lucide-react";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { useInView } from "@/features/landing-page/hooks";
|
||||
import { ContactForm } from "@/features/support/components";
|
||||
|
||||
export function ContactSection() {
|
||||
const [ref, isInView] = useInView();
|
||||
|
||||
return (
|
||||
<section
|
||||
id="contact"
|
||||
ref={ref as React.RefObject<HTMLElement>}
|
||||
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",
|
||||
isInView ? "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-2xl sm:text-3xl font-extrabold text-foreground">
|
||||
Tell Us What You Need
|
||||
</h2>
|
||||
<div className="rounded-2xl bg-card border border-border/60 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">
|
||||
{/* Left: Form + Contact Methods */}
|
||||
<div className="space-y-6">
|
||||
<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>
|
||||
<ContactForm className="border-0 p-0 rounded-none bg-transparent" />
|
||||
|
||||
<div className="flex flex-col gap-3 pt-2">
|
||||
<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-muted-foreground">
|
||||
Click the bottom right “Chat Button” 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-muted-foreground">
|
||||
<p className="font-semibold text-foreground">Toll Free within Japan</p>
|
||||
<p className="text-lg font-bold text-primary">0120-660-470</p>
|
||||
<p className="font-semibold text-foreground mt-1">From Overseas</p>
|
||||
<p className="text-lg font-bold text-primary">+81-3-3560-1006</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Map + Address */}
|
||||
<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-muted-foreground">
|
||||
Subway Oedo Line / Nanboku Line
|
||||
<br />
|
||||
Short walk from Exit 6, Azabu-Juban Station
|
||||
</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-muted-foreground">
|
||||
3F Azabu Maruka Bldg.,
|
||||
<br />
|
||||
3-8-2 Higashi Azabu, Minato-ku,
|
||||
<br />
|
||||
Tokyo 106-0044
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -44,12 +44,11 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
|
||||
|
||||
<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">A One Stop Solution</span>
|
||||
<span className="block text-primary mt-2">for Your IT Needs</span>
|
||||
<span className="block">Just Moved to Japan?</span>
|
||||
<span className="block text-primary mt-2">Get Connected in English</span>
|
||||
</h1>
|
||||
<p className="mt-6 text-base sm:text-lg text-muted-foreground leading-relaxed font-semibold max-w-2xl mx-auto">
|
||||
From internet and mobile to VPN and on-site tech support — we handle it all in English so
|
||||
you don't have to.
|
||||
Internet, phone, VPN and IT support — set up in days, not weeks. No Japanese needed.
|
||||
</p>
|
||||
<div
|
||||
ref={heroCTARef}
|
||||
@ -62,10 +61,10 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
|
||||
size="lg"
|
||||
rightIcon={<ArrowRight className="h-5 w-5" />}
|
||||
>
|
||||
Browse Services
|
||||
Find Your Plan
|
||||
</Button>
|
||||
<Button as="a" href="/contact" variant="pillOutline" size="lg">
|
||||
Need Assistance?
|
||||
<Button as="a" href="#contact" variant="pillOutline" size="lg">
|
||||
Talk to Us
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { useInView } from "@/features/landing-page/hooks";
|
||||
import {
|
||||
personalConversionCards,
|
||||
businessConversionCards,
|
||||
type ConversionServiceCard,
|
||||
} from "@/features/landing-page/data";
|
||||
|
||||
type Tab = "personal" | "business";
|
||||
|
||||
function ServiceConversionCard({ card }: { card: ConversionServiceCard }) {
|
||||
return (
|
||||
<Link href={card.href} className="group flex-shrink-0 w-[260px] sm:w-[280px] snap-start">
|
||||
<article
|
||||
data-service-card
|
||||
className="h-full rounded-2xl bg-card border border-border/60 px-6 py-7 shadow-sm hover:shadow-md hover:border-primary/30 transition-all duration-300 group-hover:-translate-y-1 flex flex-col"
|
||||
>
|
||||
{card.badge && (
|
||||
<span className="inline-flex self-start items-center rounded-full bg-success/10 text-success px-2.5 py-0.5 text-xs font-semibold mb-3">
|
||||
{card.badge}
|
||||
</span>
|
||||
)}
|
||||
<div className="mb-4 text-primary">{card.icon}</div>
|
||||
<p className="text-sm text-muted-foreground mb-1">{card.problemHook}</p>
|
||||
<h3 className="text-lg font-bold text-foreground mb-1">{card.title}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3 flex-grow">{card.keyBenefit}</p>
|
||||
{card.priceFrom && (
|
||||
<p className="text-lg font-bold text-primary mb-4">from {card.priceFrom}</p>
|
||||
)}
|
||||
<span className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary group-hover:gap-2.5 transition-all mt-auto">
|
||||
{card.ctaLabel}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</span>
|
||||
</article>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function ServicesCarousel() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("personal");
|
||||
const carouselRef = useRef<HTMLDivElement>(null);
|
||||
const itemWidthRef = useRef(0);
|
||||
const isScrollingRef = useRef(false);
|
||||
const autoScrollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [sectionRef, isInView] = useInView();
|
||||
|
||||
const cards = activeTab === "personal" ? personalConversionCards : businessConversionCards;
|
||||
|
||||
const computeItemWidth = useCallback(() => {
|
||||
const container = carouselRef.current;
|
||||
if (!container) return;
|
||||
const card = container.querySelector<HTMLElement>("[data-service-card]");
|
||||
if (!card) return;
|
||||
const style = getComputedStyle(container);
|
||||
const gap = Number.parseFloat(style.columnGap || style.gap || "0") || 24;
|
||||
itemWidthRef.current = card.getBoundingClientRect().width + gap;
|
||||
}, []);
|
||||
|
||||
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 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;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
computeItemWidth();
|
||||
window.addEventListener("resize", computeItemWidth);
|
||||
startAutoScroll();
|
||||
return () => {
|
||||
window.removeEventListener("resize", computeItemWidth);
|
||||
stopAutoScroll();
|
||||
};
|
||||
}, [computeItemWidth, startAutoScroll, stopAutoScroll]);
|
||||
|
||||
// Reset scroll position when tab changes
|
||||
useEffect(() => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.scrollTo({ left: 0, behavior: "smooth" });
|
||||
}
|
||||
computeItemWidth();
|
||||
}, [activeTab, computeItemWidth]);
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
scrollByOne(-1);
|
||||
startAutoScroll();
|
||||
}, [scrollByOne, startAutoScroll]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
scrollByOne(1);
|
||||
startAutoScroll();
|
||||
}, [scrollByOne, startAutoScroll]);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef as React.RefObject<HTMLElement>}
|
||||
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",
|
||||
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">
|
||||
{/* Header + Tabs */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-foreground">Our Services</h2>
|
||||
<p className="mt-1 text-base text-muted-foreground">
|
||||
Everything you need to stay connected in Japan
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex bg-muted rounded-full p-1 self-start">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("personal")}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-semibold rounded-full transition-all",
|
||||
activeTab === "personal"
|
||||
? "bg-foreground text-background shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
For You
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("business")}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-semibold rounded-full transition-all",
|
||||
activeTab === "business"
|
||||
? "bg-foreground text-background shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
For Business
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Carousel */}
|
||||
<div className="relative" onMouseEnter={stopAutoScroll} onMouseLeave={startAutoScroll}>
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="flex gap-5 overflow-x-auto scroll-smooth pb-4 snap-x snap-mandatory"
|
||||
style={{
|
||||
scrollbarWidth: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
}}
|
||||
>
|
||||
{cards.map(card => (
|
||||
<ServiceConversionCard key={card.title} card={card} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Scroll left"
|
||||
onClick={handlePrev}
|
||||
className="h-9 w-9 rounded-full border border-border bg-card text-foreground shadow-sm hover:bg-muted transition-colors flex items-center justify-center"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Scroll right"
|
||||
onClick={handleNext}
|
||||
className="h-9 w-9 rounded-full border border-border bg-card text-foreground shadow-sm hover:bg-muted transition-colors flex items-center justify-center"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ServiceCard } from "@/components/molecules";
|
||||
import { cn } from "@/shared/utils";
|
||||
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={cn(
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Download } from "lucide-react";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { useInView } from "@/features/landing-page/hooks";
|
||||
import { supportDownloads } from "@/features/landing-page/data";
|
||||
|
||||
export function SupportDownloadsSection() {
|
||||
const [ref, isInView] = useInView();
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={ref as React.RefObject<HTMLElement>}
|
||||
className={cn(
|
||||
"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-5xl px-6 sm:px-10 lg:px-14">
|
||||
<h2 className="text-center text-2xl sm:text-3xl font-extrabold text-foreground tracking-tight mb-2">
|
||||
Remote Support
|
||||
</h2>
|
||||
<p className="text-center text-muted-foreground mb-8">
|
||||
Download one of these tools so our technicians can assist you remotely.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{supportDownloads.map(tool => (
|
||||
<a
|
||||
key={tool.title}
|
||||
href={tool.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group bg-card rounded-2xl border border-border/60 p-6 hover:border-primary/40 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="w-16 h-16 rounded-xl bg-muted/30 flex items-center justify-center shrink-0 overflow-hidden">
|
||||
<Image
|
||||
src={tool.image}
|
||||
alt={tool.title}
|
||||
width={48}
|
||||
height={48}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-bold text-foreground group-hover:text-primary transition-colors">
|
||||
{tool.title}
|
||||
</h3>
|
||||
<Download className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed mb-2">
|
||||
{tool.description}
|
||||
</p>
|
||||
<p className="text-xs font-medium text-primary">{tool.useCase}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
// Landing page sections
|
||||
export { HeroSection } from "./HeroSection";
|
||||
export { TrustStrip } from "./TrustStrip";
|
||||
export { ServicesGrid } from "./ServicesGrid";
|
||||
export { ServicesCarousel } from "./ServicesCarousel";
|
||||
export { WhyUsSection } from "./WhyUsSection";
|
||||
export { CTABanner } from "./CTABanner";
|
||||
export { SupportDownloadsSection } from "./SupportDownloadsSection";
|
||||
export { ContactSection } from "./ContactSection";
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
export {
|
||||
type ServiceCategory,
|
||||
type ServiceItem,
|
||||
type ConversionServiceCard,
|
||||
type LandingServiceItem,
|
||||
personalServices,
|
||||
businessServices,
|
||||
personalConversionCards,
|
||||
businessConversionCards,
|
||||
services,
|
||||
supportDownloads,
|
||||
mobileQuickServices,
|
||||
|
||||
@ -23,6 +23,17 @@ export interface ServiceItem {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface ConversionServiceCard {
|
||||
title: string;
|
||||
problemHook: string;
|
||||
keyBenefit: string;
|
||||
priceFrom?: string;
|
||||
badge?: string;
|
||||
icon: React.ReactNode;
|
||||
href: string;
|
||||
ctaLabel: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SERVICE DATA
|
||||
// =============================================================================
|
||||
@ -79,6 +90,92 @@ export const businessServices: ServiceItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// CONVERSION CARD DATA (for ServicesCarousel)
|
||||
// =============================================================================
|
||||
|
||||
export const personalConversionCards: ConversionServiceCard[] = [
|
||||
{
|
||||
title: "Internet Plans",
|
||||
problemHook: "Need reliable internet?",
|
||||
keyBenefit: "NTT Fiber up to 10Gbps",
|
||||
priceFrom: "¥3,200/mo",
|
||||
icon: <Wifi className="h-7 w-7" />,
|
||||
href: "/services/internet",
|
||||
ctaLabel: "View Plans",
|
||||
},
|
||||
{
|
||||
title: "Phone Plans",
|
||||
problemHook: "Need a SIM card?",
|
||||
keyBenefit: "Docomo network coverage",
|
||||
priceFrom: "¥1,100/mo",
|
||||
badge: "1st month free",
|
||||
icon: <Smartphone className="h-7 w-7" />,
|
||||
href: "/services/sim",
|
||||
ctaLabel: "View Plans",
|
||||
},
|
||||
{
|
||||
title: "VPN Service",
|
||||
problemHook: "Missing shows from home?",
|
||||
keyBenefit: "Stream US & UK content",
|
||||
priceFrom: "¥2,500/mo",
|
||||
icon: <Lock className="h-7 w-7" />,
|
||||
href: "/services/vpn",
|
||||
ctaLabel: "View Plans",
|
||||
},
|
||||
{
|
||||
title: "Onsite Support",
|
||||
problemHook: "Need hands-on help?",
|
||||
keyBenefit: "English-speaking technicians",
|
||||
icon: <Wrench className="h-7 w-7" />,
|
||||
href: "/services/onsite",
|
||||
ctaLabel: "Learn More",
|
||||
},
|
||||
];
|
||||
|
||||
export const businessConversionCards: ConversionServiceCard[] = [
|
||||
{
|
||||
title: "Office LAN Setup",
|
||||
problemHook: "Setting up an office?",
|
||||
keyBenefit: "Complete network infrastructure",
|
||||
icon: <Server className="h-7 w-7" />,
|
||||
href: "/services/business",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
{
|
||||
title: "Tech Support",
|
||||
problemHook: "Need ongoing IT help?",
|
||||
keyBenefit: "Onsite & remote support",
|
||||
icon: <Wrench className="h-7 w-7" />,
|
||||
href: "/services/onsite",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
{
|
||||
title: "Dedicated Internet",
|
||||
problemHook: "Need guaranteed bandwidth?",
|
||||
keyBenefit: "Enterprise-grade connectivity",
|
||||
icon: <Building2 className="h-7 w-7" />,
|
||||
href: "/services/business",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
{
|
||||
title: "Data Center",
|
||||
problemHook: "Need hosting in Japan?",
|
||||
keyBenefit: "Secure, reliable infrastructure",
|
||||
icon: <Shield className="h-7 w-7" />,
|
||||
href: "/services/business",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
{
|
||||
title: "Website Services",
|
||||
problemHook: "Need a web presence?",
|
||||
keyBenefit: "Construction & maintenance",
|
||||
icon: <Code className="h-7 w-7" />,
|
||||
href: "/services/business",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
];
|
||||
|
||||
export const services = [
|
||||
{
|
||||
title: "Internet Plans",
|
||||
|
||||
@ -6,9 +6,11 @@ import { useStickyCta } from "@/features/landing-page/hooks";
|
||||
import {
|
||||
HeroSection,
|
||||
TrustStrip,
|
||||
ServicesGrid,
|
||||
ServicesCarousel,
|
||||
WhyUsSection,
|
||||
CTABanner,
|
||||
SupportDownloadsSection,
|
||||
ContactSection,
|
||||
} from "@/features/landing-page/components";
|
||||
|
||||
export function PublicLandingView() {
|
||||
@ -18,9 +20,11 @@ export function PublicLandingView() {
|
||||
<div className="space-y-0 pb-8 pt-0">
|
||||
<HeroSection heroCTARef={heroCTARef} />
|
||||
<TrustStrip />
|
||||
<ServicesGrid />
|
||||
<ServicesCarousel />
|
||||
<WhyUsSection />
|
||||
<CTABanner />
|
||||
<SupportDownloadsSection />
|
||||
<ContactSection />
|
||||
|
||||
{/* Sticky Mobile CTA */}
|
||||
{showStickyCTA && (
|
||||
@ -33,7 +37,7 @@ export function PublicLandingView() {
|
||||
rightIcon={<ArrowRight className="h-5 w-5" />}
|
||||
className="w-full shadow-lg"
|
||||
>
|
||||
Browse Services
|
||||
Find Your Plan
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button, Input } from "@/components/atoms";
|
||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
@ -11,7 +10,7 @@ import {
|
||||
publicContactRequestSchema,
|
||||
type PublicContactRequest,
|
||||
} from "@customer-portal/domain/support";
|
||||
import { apiClient, ApiError, isApiError } from "@/core/api";
|
||||
import { apiClient } from "@/core/api";
|
||||
import { cn } from "@/shared/utils";
|
||||
|
||||
interface ContactFormProps {
|
||||
@ -20,25 +19,10 @@ interface ContactFormProps {
|
||||
|
||||
export function ContactForm({ className }: ContactFormProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = useCallback(async (data: PublicContactRequest) => {
|
||||
setSubmitError(null);
|
||||
|
||||
try {
|
||||
await apiClient.POST("/api/support/contact", { body: data });
|
||||
setIsSubmitted(true);
|
||||
} catch (error) {
|
||||
if (isApiError(error)) {
|
||||
setSubmitError(error.message || "Failed to send message");
|
||||
return;
|
||||
}
|
||||
if (error instanceof ApiError) {
|
||||
setSubmitError(error.message || "Failed to send message");
|
||||
return;
|
||||
}
|
||||
setSubmitError(error instanceof Error ? error.message : "Failed to send message");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const form = useZodForm<PublicContactRequest>({
|
||||
@ -76,10 +60,10 @@ export function ContactForm({ className }: ContactFormProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("bg-white rounded-2xl border border-border/60 p-6", className)}>
|
||||
{submitError && (
|
||||
<div className={cn("bg-card rounded-2xl border border-border/60 p-6", className)}>
|
||||
{form.submitError && (
|
||||
<AlertBanner variant="error" title="Error" className="mb-6">
|
||||
{submitError}
|
||||
{form.submitError}
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
@ -169,11 +153,7 @@ export function ContactForm({ className }: ContactFormProps) {
|
||||
</form>
|
||||
|
||||
<p className="text-xs text-muted-foreground mt-4 pt-4 border-t border-border/60">
|
||||
By submitting, you agree to our{" "}
|
||||
<Link href="#" className="text-primary hover:underline">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
. We typically respond within 24 hours.
|
||||
We typically respond within 24 hours.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Mail, MapPin, Phone, MessageSquare, Clock, Send, ExternalLink } from "lucide-react";
|
||||
import { ContactForm } from "@/features/support/components";
|
||||
|
||||
@ -35,7 +33,7 @@ export function PublicContactView() {
|
||||
{/* Phone */}
|
||||
<a
|
||||
href="tel:0120-660-470"
|
||||
className="group flex items-center gap-4 bg-white rounded-2xl border border-border/60 p-5 hover:border-primary/40 hover:shadow-md transition-all duration-200"
|
||||
className="group flex items-center gap-4 bg-card rounded-2xl border border-border/60 p-5 hover:border-primary/40 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="h-11 w-11 rounded-xl bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-colors shrink-0">
|
||||
<Phone className="h-5 w-5" />
|
||||
@ -50,34 +48,20 @@ export function PublicContactView() {
|
||||
</a>
|
||||
|
||||
{/* Live Chat */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
/* Trigger chat */
|
||||
}}
|
||||
className="group flex items-center gap-4 bg-white rounded-2xl border border-border/60 p-5 hover:border-blue-500/40 hover:shadow-md transition-all duration-200 text-left w-full"
|
||||
>
|
||||
<div className="h-11 w-11 rounded-xl bg-blue-500/10 flex items-center justify-center text-blue-500 group-hover:bg-blue-500 group-hover:text-white transition-colors shrink-0">
|
||||
<div className="flex items-center gap-4 bg-card rounded-2xl border border-border/60 p-5">
|
||||
<div className="h-11 w-11 rounded-xl bg-blue-500/10 flex items-center justify-center text-blue-500 shrink-0">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-foreground text-sm group-hover:text-blue-500 transition-colors">
|
||||
Live Chat
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-success"></span>
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">Available now</span>
|
||||
<h3 className="font-bold text-foreground text-sm">Live Chat</h3>
|
||||
<p className="text-sm text-muted-foreground">Available during business hours</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Email */}
|
||||
<a
|
||||
href="mailto:support@assist-solutions.jp"
|
||||
className="group flex items-center gap-4 bg-white rounded-2xl border border-border/60 p-5 hover:border-emerald-500/40 hover:shadow-md transition-all duration-200"
|
||||
className="group flex items-center gap-4 bg-card rounded-2xl border border-border/60 p-5 hover:border-emerald-500/40 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="h-11 w-11 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500 group-hover:bg-emerald-500 group-hover:text-white transition-colors shrink-0">
|
||||
<Mail className="h-5 w-5" />
|
||||
@ -100,7 +84,7 @@ export function PublicContactView() {
|
||||
</div>
|
||||
|
||||
{/* Office Location */}
|
||||
<div className="bg-white rounded-2xl border border-border/60 p-5">
|
||||
<div className="bg-card rounded-2xl border border-border/60 p-5">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<MapPin className="h-5 w-5 text-primary" />
|
||||
<h3 className="font-bold text-foreground text-sm">Our Office</h3>
|
||||
@ -140,5 +124,3 @@ export function PublicContactView() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicContactView;
|
||||
|
||||
@ -30,7 +30,6 @@ const KNOWLEDGE_BASE_CATEGORIES = [
|
||||
icon: Wifi,
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-500/10",
|
||||
hoverBorder: "hover:border-blue-500/40",
|
||||
},
|
||||
{
|
||||
title: "Phone & SIM",
|
||||
@ -38,7 +37,6 @@ const KNOWLEDGE_BASE_CATEGORIES = [
|
||||
icon: Smartphone,
|
||||
color: "text-green-500",
|
||||
bgColor: "bg-green-500/10",
|
||||
hoverBorder: "hover:border-green-500/40",
|
||||
},
|
||||
{
|
||||
title: "VPN & Streaming",
|
||||
@ -46,7 +44,6 @@ const KNOWLEDGE_BASE_CATEGORIES = [
|
||||
icon: Lock,
|
||||
color: "text-purple-500",
|
||||
bgColor: "bg-purple-500/10",
|
||||
hoverBorder: "hover:border-purple-500/40",
|
||||
},
|
||||
{
|
||||
title: "Business Solutions",
|
||||
@ -54,7 +51,6 @@ const KNOWLEDGE_BASE_CATEGORIES = [
|
||||
icon: Building2,
|
||||
color: "text-orange-500",
|
||||
bgColor: "bg-orange-500/10",
|
||||
hoverBorder: "hover:border-orange-500/40",
|
||||
},
|
||||
{
|
||||
title: "Billing & Account",
|
||||
@ -62,7 +58,6 @@ const KNOWLEDGE_BASE_CATEGORIES = [
|
||||
icon: CreditCard,
|
||||
color: "text-pink-500",
|
||||
bgColor: "bg-pink-500/10",
|
||||
hoverBorder: "hover:border-pink-500/40",
|
||||
},
|
||||
{
|
||||
title: "General Tech Support",
|
||||
@ -70,7 +65,6 @@ const KNOWLEDGE_BASE_CATEGORIES = [
|
||||
icon: Wrench,
|
||||
color: "text-amber-500",
|
||||
bgColor: "bg-amber-500/10",
|
||||
hoverBorder: "hover:border-amber-500/40",
|
||||
},
|
||||
];
|
||||
|
||||
@ -129,19 +123,15 @@ export function PublicSupportView() {
|
||||
</div>
|
||||
|
||||
{/* Knowledge Base Categories */}
|
||||
<section className="mb-16">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">Browse by Topic</h2>
|
||||
<section className="mb-16" aria-labelledby="kb-heading">
|
||||
<h2 id="kb-heading" className="text-2xl font-bold text-foreground mb-6 text-center">
|
||||
Browse by Topic
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{KNOWLEDGE_BASE_CATEGORIES.map(category => {
|
||||
const Icon = category.icon;
|
||||
return (
|
||||
<div
|
||||
key={category.title}
|
||||
className={cn(
|
||||
"bg-white rounded-2xl border border-border/60 p-6 transition-all duration-200 hover:shadow-md",
|
||||
category.hoverBorder
|
||||
)}
|
||||
>
|
||||
<div key={category.title} className="bg-card rounded-2xl border border-border/60 p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
@ -166,8 +156,11 @@ export function PublicSupportView() {
|
||||
</section>
|
||||
|
||||
{/* Remote Support Tools */}
|
||||
<section className="mb-16">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-2 text-center">
|
||||
<section className="mb-16" aria-labelledby="remote-tools-heading">
|
||||
<h2
|
||||
id="remote-tools-heading"
|
||||
className="text-2xl font-bold text-foreground mb-2 text-center"
|
||||
>
|
||||
Remote Support Tools
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-center mb-6">
|
||||
@ -180,7 +173,7 @@ export function PublicSupportView() {
|
||||
href={tool.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group bg-white rounded-2xl border border-border/60 p-6 hover:border-primary/40 hover:shadow-md transition-all duration-200"
|
||||
className="group bg-card rounded-2xl border border-border/60 p-6 hover:border-primary/40 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="w-16 h-16 rounded-xl bg-muted/30 flex items-center justify-center shrink-0 overflow-hidden">
|
||||
@ -211,8 +204,11 @@ export function PublicSupportView() {
|
||||
</section>
|
||||
|
||||
{/* FAQ */}
|
||||
<section className="mb-16">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 text-center flex items-center justify-center gap-2">
|
||||
<section className="mb-16" aria-labelledby="faq-heading">
|
||||
<h2
|
||||
id="faq-heading"
|
||||
className="text-2xl font-bold text-foreground mb-6 text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<div className="max-w-3xl mx-auto space-y-3">
|
||||
@ -221,10 +217,12 @@ export function PublicSupportView() {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-xl border border-border/60 overflow-hidden"
|
||||
className="bg-card rounded-xl border border-border/60 overflow-hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={`faq-answer-${index}`}
|
||||
onClick={() => setExpandedFaq(isExpanded ? null : index)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
@ -237,7 +235,7 @@ export function PublicSupportView() {
|
||||
/>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4">
|
||||
<div id={`faq-answer-${index}`} role="region" className="px-4 pb-4">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{item.answer}</p>
|
||||
</div>
|
||||
)}
|
||||
@ -248,12 +246,14 @@ export function PublicSupportView() {
|
||||
</section>
|
||||
|
||||
{/* Contact Form Fallback */}
|
||||
<section className="mb-12">
|
||||
<section className="mb-12" aria-labelledby="contact-heading">
|
||||
<div className="text-center mb-6">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-xl mb-3 text-primary">
|
||||
<Send className="h-6 w-6" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-foreground">Still Need Help?</h2>
|
||||
<h2 id="contact-heading" className="text-2xl font-bold text-foreground">
|
||||
Still Need Help?
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Send us a message and we'll get back to you within 24 hours.
|
||||
</p>
|
||||
@ -279,5 +279,3 @@ export function PublicSupportView() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicSupportView;
|
||||
|
||||
930
docs/plans/2026-03-04-landing-page-conversion-overhaul.md
Normal file
930
docs/plans/2026-03-04-landing-page-conversion-overhaul.md
Normal file
@ -0,0 +1,930 @@
|
||||
# Landing Page Conversion Overhaul — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Restructure the public landing page into a conversion funnel that drives visitors to `/services`, with tabbed services carousel, support downloads, and full contact section restored.
|
||||
|
||||
**Architecture:** Replace static ServicesGrid with tabbed carousel (For You / For Business) using conversion-oriented cards with pricing. Restore full contact section (form + map + phone + address) at page bottom. Repurpose CTA Banner. Delete `/help` route. Update footer links.
|
||||
|
||||
**Tech Stack:** Next.js 15, React 19, Tailwind CSS, lucide-react icons, existing `ContactForm` component
|
||||
|
||||
**Design doc:** `docs/plans/2026-03-04-public-pages-restructuring-v2-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Extend Service Data with Conversion Card Fields
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/features/landing-page/data/services.tsx`
|
||||
|
||||
**Step 1: Add conversion card types and data**
|
||||
|
||||
Add a new `ConversionServiceCard` interface and update the `personalServices` and `businessServices` arrays with conversion fields. Keep existing exports intact for backward compatibility (other pages may use them).
|
||||
|
||||
Add after the existing `ServiceItem` interface (~line 24):
|
||||
|
||||
```tsx
|
||||
export interface ConversionServiceCard {
|
||||
title: string;
|
||||
problemHook: string;
|
||||
keyBenefit: string;
|
||||
priceFrom?: string;
|
||||
badge?: string;
|
||||
icon: React.ReactNode;
|
||||
href: string;
|
||||
ctaLabel: string;
|
||||
}
|
||||
```
|
||||
|
||||
Add new conversion data arrays (after `businessServices`, before `services`):
|
||||
|
||||
```tsx
|
||||
export const personalConversionCards: ConversionServiceCard[] = [
|
||||
{
|
||||
title: "Internet Plans",
|
||||
problemHook: "Need reliable internet?",
|
||||
keyBenefit: "NTT Fiber up to 10Gbps",
|
||||
priceFrom: "¥3,200/mo",
|
||||
icon: <Wifi className="h-7 w-7" />,
|
||||
href: "/services/internet",
|
||||
ctaLabel: "View Plans",
|
||||
},
|
||||
{
|
||||
title: "Phone Plans",
|
||||
problemHook: "Need a SIM card?",
|
||||
keyBenefit: "Docomo network coverage",
|
||||
priceFrom: "¥1,100/mo",
|
||||
badge: "1st month free",
|
||||
icon: <Smartphone className="h-7 w-7" />,
|
||||
href: "/services/sim",
|
||||
ctaLabel: "View Plans",
|
||||
},
|
||||
{
|
||||
title: "VPN Service",
|
||||
problemHook: "Missing shows from home?",
|
||||
keyBenefit: "Stream US & UK content",
|
||||
priceFrom: "¥2,500/mo",
|
||||
icon: <Lock className="h-7 w-7" />,
|
||||
href: "/services/vpn",
|
||||
ctaLabel: "View Plans",
|
||||
},
|
||||
{
|
||||
title: "Onsite Support",
|
||||
problemHook: "Need hands-on help?",
|
||||
keyBenefit: "English-speaking technicians",
|
||||
icon: <Wrench className="h-7 w-7" />,
|
||||
href: "/services/onsite",
|
||||
ctaLabel: "Learn More",
|
||||
},
|
||||
];
|
||||
|
||||
export const businessConversionCards: ConversionServiceCard[] = [
|
||||
{
|
||||
title: "Office LAN Setup",
|
||||
problemHook: "Setting up an office?",
|
||||
keyBenefit: "Complete network infrastructure",
|
||||
icon: <Server className="h-7 w-7" />,
|
||||
href: "/services/business",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
{
|
||||
title: "Tech Support",
|
||||
problemHook: "Need ongoing IT help?",
|
||||
keyBenefit: "Onsite & remote support",
|
||||
icon: <Wrench className="h-7 w-7" />,
|
||||
href: "/services/onsite",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
{
|
||||
title: "Dedicated Internet",
|
||||
problemHook: "Need guaranteed bandwidth?",
|
||||
keyBenefit: "Enterprise-grade connectivity",
|
||||
icon: <Building2 className="h-7 w-7" />,
|
||||
href: "/services/business",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
{
|
||||
title: "Data Center",
|
||||
problemHook: "Need hosting in Japan?",
|
||||
keyBenefit: "Secure, reliable infrastructure",
|
||||
icon: <Shield className="h-7 w-7" />,
|
||||
href: "/services/business",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
{
|
||||
title: "Website Services",
|
||||
problemHook: "Need a web presence?",
|
||||
keyBenefit: "Construction & maintenance",
|
||||
icon: <Code className="h-7 w-7" />,
|
||||
href: "/services/business",
|
||||
ctaLabel: "Get a Quote",
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal -- --no-warn`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
feat: add conversion card data for landing page services carousel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create ServicesCarousel Component
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/portal/src/features/landing-page/components/ServicesCarousel.tsx`
|
||||
|
||||
**Step 1: Create the tabbed carousel component**
|
||||
|
||||
This component has:
|
||||
|
||||
- Tab switcher ("For You" / "For Business")
|
||||
- Horizontal scrolling carousel with auto-scroll
|
||||
- Conversion-oriented service cards with problem hook, benefit, price, badge, CTA
|
||||
- Prev/next navigation buttons
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { useInView } from "@/features/landing-page/hooks";
|
||||
import {
|
||||
personalConversionCards,
|
||||
businessConversionCards,
|
||||
type ConversionServiceCard,
|
||||
} from "@/features/landing-page/data";
|
||||
|
||||
type Tab = "personal" | "business";
|
||||
|
||||
function ServiceConversionCard({ card }: { card: ConversionServiceCard }) {
|
||||
return (
|
||||
<Link href={card.href} className="group flex-shrink-0 w-[260px] sm:w-[280px] snap-start">
|
||||
<article
|
||||
data-service-card
|
||||
className="h-full rounded-2xl bg-card border border-border/60 px-6 py-7 shadow-sm hover:shadow-md hover:border-primary/30 transition-all duration-300 group-hover:-translate-y-1 flex flex-col"
|
||||
>
|
||||
{card.badge && (
|
||||
<span className="inline-flex self-start items-center rounded-full bg-success/10 text-success px-2.5 py-0.5 text-xs font-semibold mb-3">
|
||||
{card.badge}
|
||||
</span>
|
||||
)}
|
||||
<div className="mb-4 text-primary">{card.icon}</div>
|
||||
<p className="text-sm text-muted-foreground mb-1">{card.problemHook}</p>
|
||||
<h3 className="text-lg font-bold text-foreground mb-1">{card.title}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3 flex-grow">{card.keyBenefit}</p>
|
||||
{card.priceFrom && (
|
||||
<p className="text-lg font-bold text-primary mb-4">from {card.priceFrom}</p>
|
||||
)}
|
||||
<span className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary group-hover:gap-2.5 transition-all mt-auto">
|
||||
{card.ctaLabel}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</span>
|
||||
</article>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function ServicesCarousel() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("personal");
|
||||
const carouselRef = useRef<HTMLDivElement>(null);
|
||||
const itemWidthRef = useRef(0);
|
||||
const isScrollingRef = useRef(false);
|
||||
const autoScrollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [sectionRef, isInView] = useInView();
|
||||
|
||||
const cards = activeTab === "personal" ? personalConversionCards : businessConversionCards;
|
||||
|
||||
const computeItemWidth = useCallback(() => {
|
||||
const container = carouselRef.current;
|
||||
if (!container) return;
|
||||
const card = container.querySelector<HTMLElement>("[data-service-card]");
|
||||
if (!card) return;
|
||||
const style = getComputedStyle(container);
|
||||
const gap = parseFloat(style.columnGap || style.gap || "0") || 24;
|
||||
itemWidthRef.current = card.getBoundingClientRect().width + gap;
|
||||
}, []);
|
||||
|
||||
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 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;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
computeItemWidth();
|
||||
window.addEventListener("resize", computeItemWidth);
|
||||
startAutoScroll();
|
||||
return () => {
|
||||
window.removeEventListener("resize", computeItemWidth);
|
||||
stopAutoScroll();
|
||||
};
|
||||
}, [computeItemWidth, startAutoScroll, stopAutoScroll]);
|
||||
|
||||
// Reset scroll position when tab changes
|
||||
useEffect(() => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.scrollTo({ left: 0, behavior: "smooth" });
|
||||
}
|
||||
computeItemWidth();
|
||||
}, [activeTab, computeItemWidth]);
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
scrollByOne(-1);
|
||||
startAutoScroll();
|
||||
}, [scrollByOne, startAutoScroll]);
|
||||
const handleNext = useCallback(() => {
|
||||
scrollByOne(1);
|
||||
startAutoScroll();
|
||||
}, [scrollByOne, startAutoScroll]);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef as React.RefObject<HTMLElement>}
|
||||
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",
|
||||
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">
|
||||
{/* Header + Tabs */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-foreground">Our Services</h2>
|
||||
<p className="mt-1 text-base text-muted-foreground">
|
||||
Everything you need to stay connected in Japan
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex bg-muted rounded-full p-1 self-start">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("personal")}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-semibold rounded-full transition-all",
|
||||
activeTab === "personal"
|
||||
? "bg-foreground text-background shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
For You
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("business")}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-semibold rounded-full transition-all",
|
||||
activeTab === "business"
|
||||
? "bg-foreground text-background shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
For Business
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Carousel */}
|
||||
<div className="relative" onMouseEnter={stopAutoScroll} onMouseLeave={startAutoScroll}>
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="flex gap-5 overflow-x-auto scroll-smooth pb-4 snap-x snap-mandatory"
|
||||
style={{ scrollbarWidth: "none", WebkitOverflowScrolling: "touch" }}
|
||||
>
|
||||
{cards.map(card => (
|
||||
<ServiceConversionCard key={card.title} card={card} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Scroll left"
|
||||
onClick={handlePrev}
|
||||
className="h-9 w-9 rounded-full border border-border bg-card text-foreground shadow-sm hover:bg-muted transition-colors flex items-center justify-center"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Scroll right"
|
||||
onClick={handleNext}
|
||||
className="h-9 w-9 rounded-full border border-border bg-card text-foreground shadow-sm hover:bg-muted transition-colors flex items-center justify-center"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal -- --no-warn`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
feat: create ServicesCarousel with tabbed conversion cards
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create SupportDownloadsSection Component
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/portal/src/features/landing-page/components/SupportDownloadsSection.tsx`
|
||||
|
||||
**Step 1: Create the component**
|
||||
|
||||
Extracted from old landing page and from the support page pattern. Uses existing `supportDownloads` data.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Download } from "lucide-react";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { useInView } from "@/features/landing-page/hooks";
|
||||
import { supportDownloads } from "@/features/landing-page/data";
|
||||
|
||||
export function SupportDownloadsSection() {
|
||||
const [ref, isInView] = useInView();
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={ref as React.RefObject<HTMLElement>}
|
||||
className={cn(
|
||||
"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-5xl px-6 sm:px-10 lg:px-14">
|
||||
<h2 className="text-center text-2xl sm:text-3xl font-extrabold text-foreground tracking-tight mb-2">
|
||||
Remote Support
|
||||
</h2>
|
||||
<p className="text-center text-muted-foreground mb-8">
|
||||
Download one of these tools so our technicians can assist you remotely.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{supportDownloads.map(tool => (
|
||||
<a
|
||||
key={tool.title}
|
||||
href={tool.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group bg-card rounded-2xl border border-border/60 p-6 hover:border-primary/40 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="w-16 h-16 rounded-xl bg-muted/30 flex items-center justify-center shrink-0 overflow-hidden">
|
||||
<Image
|
||||
src={tool.image}
|
||||
alt={tool.title}
|
||||
width={48}
|
||||
height={48}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-bold text-foreground group-hover:text-primary transition-colors">
|
||||
{tool.title}
|
||||
</h3>
|
||||
<Download className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed mb-2">
|
||||
{tool.description}
|
||||
</p>
|
||||
<p className="text-xs font-medium text-primary">{tool.useCase}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal -- --no-warn`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
feat: create SupportDownloadsSection component for landing page
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Create ContactSection Component
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/portal/src/features/landing-page/components/ContactSection.tsx`
|
||||
|
||||
**Step 1: Create the full contact section**
|
||||
|
||||
Restored from the old landing page layout — form on the left with chat/phone info, map + address on the right. Uses the existing `ContactForm` component.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { Mail, MapPin, MessageSquare, PhoneCall, Train } from "lucide-react";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { useInView } from "@/features/landing-page/hooks";
|
||||
import { ContactForm } from "@/features/support/components";
|
||||
|
||||
export function ContactSection() {
|
||||
const [ref, isInView] = useInView();
|
||||
|
||||
return (
|
||||
<section
|
||||
id="contact"
|
||||
ref={ref as React.RefObject<HTMLElement>}
|
||||
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",
|
||||
isInView ? "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-2xl sm:text-3xl font-extrabold text-foreground">
|
||||
Tell Us What You Need
|
||||
</h2>
|
||||
<div className="rounded-2xl bg-card border border-border/60 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">
|
||||
{/* Left: Form + Contact Methods */}
|
||||
<div className="space-y-6">
|
||||
<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>
|
||||
<ContactForm className="border-0 p-0 rounded-none bg-transparent" />
|
||||
|
||||
<div className="flex flex-col gap-3 pt-2">
|
||||
<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-muted-foreground">
|
||||
Click the bottom right “Chat Button” 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-muted-foreground">
|
||||
<p className="font-semibold text-foreground">Toll Free within Japan</p>
|
||||
<p className="text-lg font-bold text-primary">0120-660-470</p>
|
||||
<p className="font-semibold text-foreground mt-1">From Overseas</p>
|
||||
<p className="text-lg font-bold text-primary">+81-3-3560-1006</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Map + Address */}
|
||||
<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-muted-foreground">
|
||||
Subway Oedo Line / Nanboku Line
|
||||
<br />
|
||||
Short walk from Exit 6, Azabu-Juban Station
|
||||
</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-muted-foreground">
|
||||
3F Azabu Maruka Bldg.,
|
||||
<br />
|
||||
3-8-2 Higashi Azabu, Minato-ku,
|
||||
<br />
|
||||
Tokyo 106-0044
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Important note:** The `ContactForm` component renders its own card wrapper (`bg-card rounded-2xl border ...`). We pass `className="border-0 p-0 rounded-none bg-transparent"` to strip it since the outer card provides the wrapper here. Verify this works — if `ContactForm` doesn't pass `className` through to the outer wrapper correctly, you may need to adjust. Check `ContactForm.tsx:74` where `cn("bg-card rounded-2xl border border-border/60 p-6", className)` confirms it does merge classNames correctly.
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal -- --no-warn`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
feat: create ContactSection component with form, map, and contact info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Update HeroSection — Problem-First Copy + #contact CTA
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/features/landing-page/components/HeroSection.tsx`
|
||||
|
||||
**Step 1: Update hero heading, subtitle, and CTAs**
|
||||
|
||||
Change lines 46-70:
|
||||
|
||||
```tsx
|
||||
{
|
||||
/* OLD heading (lines 46-49): */
|
||||
}
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold leading-tight text-foreground">
|
||||
<span className="block">A One Stop Solution</span>
|
||||
<span className="block text-primary mt-2">for Your IT Needs</span>
|
||||
</h1>;
|
||||
|
||||
{
|
||||
/* NEW heading: */
|
||||
}
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold leading-tight text-foreground">
|
||||
<span className="block">Just Moved to Japan?</span>
|
||||
<span className="block text-primary mt-2">Get Connected in English</span>
|
||||
</h1>;
|
||||
```
|
||||
|
||||
```tsx
|
||||
{
|
||||
/* OLD subtitle (lines 50-53): */
|
||||
}
|
||||
<p className="mt-6 text-base sm:text-lg text-muted-foreground leading-relaxed font-semibold max-w-2xl mx-auto">
|
||||
From internet and mobile to VPN and on-site tech support — we handle it all in English so you
|
||||
don't have to.
|
||||
</p>;
|
||||
|
||||
{
|
||||
/* NEW subtitle: */
|
||||
}
|
||||
<p className="mt-6 text-base sm:text-lg text-muted-foreground leading-relaxed font-semibold max-w-2xl mx-auto">
|
||||
Internet, phone, VPN and IT support — set up in days, not weeks. No Japanese needed.
|
||||
</p>;
|
||||
```
|
||||
|
||||
```tsx
|
||||
{/* OLD CTAs (lines 58-69): */}
|
||||
<Button as="a" href="/services" variant="pill" size="lg" rightIcon={<ArrowRight className="h-5 w-5" />}>
|
||||
Browse Services
|
||||
</Button>
|
||||
<Button as="a" href="/contact" variant="pillOutline" size="lg">
|
||||
Need Assistance?
|
||||
</Button>
|
||||
|
||||
{/* NEW CTAs: */}
|
||||
<Button as="a" href="/services" variant="pill" size="lg" rightIcon={<ArrowRight className="h-5 w-5" />}>
|
||||
Find Your Plan
|
||||
</Button>
|
||||
<Button as="a" href="#contact" variant="pillOutline" size="lg">
|
||||
Talk to Us
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal -- --no-warn`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
feat: update hero to problem-first copy with #contact CTA
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Update CTABanner — Remove Phone, Update CTAs
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/features/landing-page/components/CTABanner.tsx`
|
||||
|
||||
**Step 1: Rewrite CTABanner**
|
||||
|
||||
Replace the entire file content:
|
||||
|
||||
```tsx
|
||||
import { ArrowRight } from "lucide-react";
|
||||
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"
|
||||
>
|
||||
<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?
|
||||
</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-6 flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||
<Button
|
||||
as="a"
|
||||
href="/services"
|
||||
variant="pill"
|
||||
size="lg"
|
||||
rightIcon={<ArrowRight className="h-5 w-5" />}
|
||||
>
|
||||
Find Your Plan
|
||||
</Button>
|
||||
<Button as="a" href="#contact" variant="pillOutline" size="lg">
|
||||
Talk to Us
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal -- --no-warn`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
feat: repurpose CTABanner for conversion — remove phone, update CTAs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Update Barrel Exports + Compose New Landing Page
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/features/landing-page/components/index.ts`
|
||||
- Modify: `apps/portal/src/features/landing-page/views/PublicLandingView.tsx`
|
||||
|
||||
**Step 1: Update barrel exports**
|
||||
|
||||
Replace `apps/portal/src/features/landing-page/components/index.ts`:
|
||||
|
||||
```ts
|
||||
// Landing page sections
|
||||
export { HeroSection } from "./HeroSection";
|
||||
export { TrustStrip } from "./TrustStrip";
|
||||
export { ServicesCarousel } from "./ServicesCarousel";
|
||||
export { WhyUsSection } from "./WhyUsSection";
|
||||
export { CTABanner } from "./CTABanner";
|
||||
export { SupportDownloadsSection } from "./SupportDownloadsSection";
|
||||
export { ContactSection } from "./ContactSection";
|
||||
```
|
||||
|
||||
Note: `ServicesGrid` export removed — replaced by `ServicesCarousel`.
|
||||
|
||||
**Step 2: Rewrite PublicLandingView with 7-section composition**
|
||||
|
||||
Replace the entire content of `apps/portal/src/features/landing-page/views/PublicLandingView.tsx`:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { useStickyCta } from "@/features/landing-page/hooks";
|
||||
import {
|
||||
HeroSection,
|
||||
TrustStrip,
|
||||
ServicesCarousel,
|
||||
WhyUsSection,
|
||||
CTABanner,
|
||||
SupportDownloadsSection,
|
||||
ContactSection,
|
||||
} from "@/features/landing-page/components";
|
||||
|
||||
export function PublicLandingView() {
|
||||
const { heroCTARef, showStickyCTA } = useStickyCta();
|
||||
|
||||
return (
|
||||
<div className="space-y-0 pb-8 pt-0">
|
||||
<HeroSection heroCTARef={heroCTARef} />
|
||||
<TrustStrip />
|
||||
<ServicesCarousel />
|
||||
<WhyUsSection />
|
||||
<CTABanner />
|
||||
<SupportDownloadsSection />
|
||||
<ContactSection />
|
||||
|
||||
{/* Sticky Mobile CTA */}
|
||||
{showStickyCTA && (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur-sm border-t border-border p-4 z-50 md:hidden animate-in slide-in-from-bottom-4 duration-300">
|
||||
<Button
|
||||
as="a"
|
||||
href="/services"
|
||||
variant="pill"
|
||||
size="lg"
|
||||
rightIcon={<ArrowRight className="h-5 w-5" />}
|
||||
className="w-full shadow-lg"
|
||||
>
|
||||
Find Your Plan
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal -- --no-warn`
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```
|
||||
feat: compose landing page with 7-section conversion funnel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Delete ServicesGrid + /help Route
|
||||
|
||||
**Files:**
|
||||
|
||||
- Delete: `apps/portal/src/features/landing-page/components/ServicesGrid.tsx`
|
||||
- Delete: `apps/portal/src/app/(public)/(site)/help/page.tsx`
|
||||
|
||||
**Step 1: Delete ServicesGrid.tsx**
|
||||
|
||||
This file is no longer imported anywhere (barrel export was updated in Task 7).
|
||||
|
||||
Run: `rm apps/portal/src/features/landing-page/components/ServicesGrid.tsx`
|
||||
|
||||
**Step 2: Delete /help route**
|
||||
|
||||
Run: `rm apps/portal/src/app/\(public\)/\(site\)/help/page.tsx`
|
||||
|
||||
Then check if the help directory is empty and remove it:
|
||||
|
||||
Run: `rmdir apps/portal/src/app/\(public\)/\(site\)/help/ 2>/dev/null || true`
|
||||
|
||||
**Step 3: Verify no lingering imports of ServicesGrid**
|
||||
|
||||
Run grep to confirm no file imports `ServicesGrid`:
|
||||
|
||||
```bash
|
||||
pnpm exec grep -r "ServicesGrid" apps/portal/src/ --include="*.ts" --include="*.tsx"
|
||||
```
|
||||
|
||||
Expected: no results.
|
||||
|
||||
**Step 4: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal -- --no-warn`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```
|
||||
refactor: remove ServicesGrid and /help route
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Update SiteFooter Links
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/components/organisms/SiteFooter/SiteFooter.tsx`
|
||||
|
||||
**Step 1: Split "Support & Contact" into separate links**
|
||||
|
||||
In the Company links section (lines 85-110), change the "Support & Contact" link:
|
||||
|
||||
```tsx
|
||||
{/* OLD (lines 94-101): */}
|
||||
<li>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Support & Contact
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
{/* NEW — replace with two separate links: */}
|
||||
<li>
|
||||
<Link
|
||||
href="/support"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Support
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal -- --no-warn`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
refactor: split footer "Support & Contact" into separate links
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Final Verification
|
||||
|
||||
**Step 1: Run full type check**
|
||||
|
||||
Run: `pnpm type-check`
|
||||
|
||||
Expected: no errors related to landing-page, support, or footer components.
|
||||
|
||||
**Step 2: Run full lint check**
|
||||
|
||||
Run: `pnpm lint`
|
||||
|
||||
Expected: no new errors. Warnings are acceptable.
|
||||
|
||||
**Step 3: Verify no dangling imports**
|
||||
|
||||
Run grep to confirm:
|
||||
|
||||
- No imports of `ServicesGrid` exist
|
||||
- No imports reference `/help` route
|
||||
- `ContactSection` is only imported in `PublicLandingView`
|
||||
|
||||
```bash
|
||||
pnpm exec grep -rn "ServicesGrid\|from.*help/page\|/help" apps/portal/src/ --include="*.ts" --include="*.tsx" | grep -v node_modules | grep -v ".next"
|
||||
```
|
||||
|
||||
**Step 4: Commit any fixes**
|
||||
|
||||
If there are type or lint errors, fix them and commit:
|
||||
|
||||
```
|
||||
fix: resolve lint/type errors from landing page restructuring
|
||||
```
|
||||
208
docs/plans/2026-03-04-public-pages-restructuring-v2-design.md
Normal file
208
docs/plans/2026-03-04-public-pages-restructuring-v2-design.md
Normal file
@ -0,0 +1,208 @@
|
||||
# Public Pages Restructuring v2 — Design
|
||||
|
||||
**Date**: 2026-03-04
|
||||
**Scope**: Landing page conversion overhaul, contact section restoration, routing cleanup
|
||||
|
||||
## Problem
|
||||
|
||||
The previous restructuring (v1) stripped the landing page down to an informational layout — hero, static 4-card grid, trust section, and a CTA banner with a phone number. The contact form was removed from the landing page and pushed to a standalone `/contact` page. This broke the conversion funnel:
|
||||
|
||||
- No contact form on the landing page — visitors must navigate away to reach out
|
||||
- CTA Banner leads with a phone number instead of driving visitors to services
|
||||
- Services presented as a static 4-card grid with no pricing — no hook to click through
|
||||
- Service cards are informational, not conversion-oriented
|
||||
- No smart grouping of Personal vs Business services
|
||||
- `/help` route is unnecessary
|
||||
- Support downloads (Acronis, TeamViewer) removed from landing page
|
||||
|
||||
The services page (`/services` and sub-pages) is the actual converter — where visitors select plans and enter the checkout funnel. The landing page's job is to funnel visitors there, with the contact form as a fallback for those who want to talk.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Landing Page (`/`)
|
||||
|
||||
Seven sections, structured as a conversion funnel that drives visitors to `/services`:
|
||||
|
||||
**Section 1: Hero (problem-first framing)**
|
||||
|
||||
Reframe from company-centric to visitor-centric:
|
||||
|
||||
- Heading: "Just Moved to Japan? Get Connected in English."
|
||||
- Subtitle: "Internet, phone, VPN and IT support — set up in days, not weeks. No Japanese needed."
|
||||
- Primary CTA: "Find Your Plan" → `/services`
|
||||
- Secondary CTA: "Talk to Us" → `#contact` (same-page smooth scroll)
|
||||
|
||||
**Section 2: Trust Strip (unchanged)**
|
||||
|
||||
- 20+ years | Full English | Foreign Cards | 10,000+ Customers
|
||||
- No changes to current implementation
|
||||
|
||||
**Section 3: Services Section (tabbed carousel with conversion cards)**
|
||||
|
||||
Replace the static 4-card `ServicesGrid` with a tabbed carousel:
|
||||
|
||||
- Tab switcher: "For You" | "For Business"
|
||||
- "For You" tab: Internet, SIM, VPN, Onsite (from `personalServices` data)
|
||||
- "For Business" tab: Office LAN, Tech Support, Dedicated Internet, Data Center, Web Construction (from `businessServices` data)
|
||||
- Horizontal scroll with prev/next buttons and auto-scroll
|
||||
|
||||
Each card is a conversion-oriented mini-ad:
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ "Need reliable internet?" │ ← problem hook
|
||||
│ NTT Fiber up to 10Gbps │ ← key benefit
|
||||
│ from ¥3,200/mo │ ← price anchor
|
||||
│ ★ Most Popular │ ← badge (optional)
|
||||
│ [View Plans →] │ ← CTA
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
Card data structure (extend existing `ServiceItem`):
|
||||
|
||||
```ts
|
||||
interface ConversionServiceCard {
|
||||
title: string;
|
||||
problemHook: string; // "Need reliable internet?"
|
||||
keyBenefit: string; // "NTT Fiber up to 10Gbps"
|
||||
priceFrom: string; // "from ¥3,200/mo"
|
||||
badge?: string; // "Most Popular" | "1st month free"
|
||||
icon: React.ReactNode;
|
||||
href: string;
|
||||
ctaLabel: string; // "View Plans"
|
||||
}
|
||||
```
|
||||
|
||||
**Section 4: Why Us (unchanged)**
|
||||
|
||||
- "Built on Trust and Excellence"
|
||||
- Image + trust points (English support, foreign cards, bilingual contracts)
|
||||
- Link to `/about`
|
||||
|
||||
**Section 5: CTA Banner (repurposed)**
|
||||
|
||||
- Heading: "Ready to Get Set Up?"
|
||||
- Subtitle: "No Japanese required. Our English-speaking team is here to help."
|
||||
- Primary CTA: "Find Your Plan" → `/services`
|
||||
- Secondary CTA: "Talk to Us" → `#contact`
|
||||
- Phone number removed as headline
|
||||
|
||||
**Section 6: Support Downloads**
|
||||
|
||||
Restored from old landing page. Two cards side-by-side:
|
||||
|
||||
- Acronis Quick Assist (download link)
|
||||
- TeamViewer QS (download link)
|
||||
|
||||
Uses existing `supportDownloads` data from `features/landing-page/data/services.tsx`.
|
||||
|
||||
**Section 7: Contact Section (`#contact`)**
|
||||
|
||||
Full contact section restored (like original pre-v1 landing page):
|
||||
|
||||
- Left column:
|
||||
- Contact form (uses existing `ContactForm` component)
|
||||
- "By Chat (Anytime)" — chat mention
|
||||
- "By Phone (9:30-18:00 JST)" — toll-free (0120-660-470) + international (+81-3-3560-1006)
|
||||
- Right column:
|
||||
- Google Maps embed
|
||||
- Access info card (Azabu-Juban Station, Exit 6)
|
||||
- Address card (3F Azabu Maruka Bldg., Minato-ku, Tokyo)
|
||||
|
||||
**Sticky Mobile CTA:**
|
||||
|
||||
- "Find Your Plan" → `/services`
|
||||
- Shows when hero CTA scrolls out of viewport (existing `useStickyCta` hook)
|
||||
|
||||
### 2. Routing Changes
|
||||
|
||||
| Route | Current | New |
|
||||
| ---------- | ----------------------- | ------------------------------------------------------------- |
|
||||
| `/` | Stripped-down landing | Full 7-section conversion funnel |
|
||||
| `/contact` | Standalone contact page | **No change** — stays for SEO, external links, cross-site nav |
|
||||
| `/support` | Self-service hub | **No change** |
|
||||
| `/help` | Redirects to `/contact` | **Delete** — route removed entirely |
|
||||
|
||||
### 3. Navigation Link Updates
|
||||
|
||||
| Location | Link Text | Target |
|
||||
| -------------------- | ---------------- | ----------- |
|
||||
| Header nav | "Support" | `/support` |
|
||||
| Footer | "Support" | `/support` |
|
||||
| Footer | "Contact" | `/contact` |
|
||||
| Hero primary | "Find Your Plan" | `/services` |
|
||||
| Hero secondary | "Talk to Us" | `#contact` |
|
||||
| CTA Banner primary | "Find Your Plan" | `/services` |
|
||||
| CTA Banner secondary | "Talk to Us" | `#contact` |
|
||||
| Service page CTAs | "Contact Us" | `/contact` |
|
||||
| Sticky mobile | "Find Your Plan" | `/services` |
|
||||
|
||||
### 4. Component Changes
|
||||
|
||||
**New components:**
|
||||
|
||||
- `ServicesCarousel.tsx` — tabbed (For You / For Business) horizontal carousel with conversion cards, auto-scroll, prev/next buttons
|
||||
- `ContactSection.tsx` — full contact section: form (left) + map/address (right), uses `ContactForm` component
|
||||
- `SupportDownloadsSection.tsx` — two-card grid for Acronis + TeamViewer downloads
|
||||
|
||||
**Modified components:**
|
||||
|
||||
- `PublicLandingView.tsx` — new 7-section composition replacing current 5-section layout
|
||||
- `CTABanner.tsx` — remove phone number headline, update CTAs to "Find Your Plan" + "Talk to Us"
|
||||
- `HeroSection.tsx` — problem-first copy, secondary CTA targets `#contact` instead of `/contact`
|
||||
- `SiteFooter.tsx` — ensure "Support" → `/support` and "Contact" → `/contact` as separate links
|
||||
- `features/landing-page/data/services.tsx` — extend service data with `problemHook`, `keyBenefit`, `priceFrom`, `badge` fields for conversion cards
|
||||
|
||||
**Deleted:**
|
||||
|
||||
- `ServicesGrid.tsx` — replaced by `ServicesCarousel.tsx`
|
||||
- `app/(public)/(site)/help/page.tsx` — route removed
|
||||
|
||||
**Unchanged:**
|
||||
|
||||
- `PublicContactView.tsx` — standalone contact page stays as-is
|
||||
- `PublicSupportView.tsx` — support page stays as-is (KB cards decorative)
|
||||
- `ContactForm.tsx` — reused in landing page contact section + contact page + support page
|
||||
- All service pages, about, blog, auth pages — no changes
|
||||
- `TrustStrip.tsx` — no changes
|
||||
- `WhyUsSection.tsx` — no changes
|
||||
|
||||
## Service Card Pricing Data
|
||||
|
||||
Pricing to display on conversion cards (sourced from service pages):
|
||||
|
||||
| Service | Price From | Badge |
|
||||
| ------------------ | --------------- | -------------- |
|
||||
| Internet | ¥3,200/mo | — |
|
||||
| SIM | ¥1,100/mo | 1st month free |
|
||||
| VPN | ¥2,500/mo | — |
|
||||
| Onsite Support | — (quote-based) | — |
|
||||
| Office LAN Setup | — (quote-based) | — |
|
||||
| Tech Support | — (quote-based) | — |
|
||||
| Dedicated Internet | — (quote-based) | — |
|
||||
| Data Center | — (quote-based) | — |
|
||||
| Web Construction | — (quote-based) | — |
|
||||
|
||||
Quote-based services show "Get a Quote" instead of a price.
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
### New files
|
||||
|
||||
- `features/landing-page/components/ServicesCarousel.tsx`
|
||||
- `features/landing-page/components/ContactSection.tsx`
|
||||
- `features/landing-page/components/SupportDownloadsSection.tsx`
|
||||
|
||||
### Modified files
|
||||
|
||||
- `features/landing-page/views/PublicLandingView.tsx`
|
||||
- `features/landing-page/components/HeroSection.tsx`
|
||||
- `features/landing-page/components/CTABanner.tsx`
|
||||
- `features/landing-page/components/index.ts`
|
||||
- `features/landing-page/data/services.tsx`
|
||||
- `components/organisms/SiteFooter/SiteFooter.tsx`
|
||||
|
||||
### Deleted files
|
||||
|
||||
- `features/landing-page/components/ServicesGrid.tsx`
|
||||
- `app/(public)/(site)/help/page.tsx`
|
||||
Loading…
x
Reference in New Issue
Block a user