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:
barsa 2026-03-04 14:50:45 +09:00
parent 5a66adb7e6
commit ee85426743
18 changed files with 1676 additions and 166 deletions

View File

@ -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");
}

View File

@ -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>

View File

@ -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"
>

View File

@ -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>

View File

@ -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 &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-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>
);
}

View File

@ -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&apos;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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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";

View File

@ -1,9 +1,12 @@
export {
type ServiceCategory,
type ServiceItem,
type ConversionServiceCard,
type LandingServiceItem,
personalServices,
businessServices,
personalConversionCards,
businessConversionCards,
services,
supportDownloads,
mobileQuickServices,

View File

@ -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",

View File

@ -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>
)}

View File

@ -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");
}
await apiClient.POST("/api/support/contact", { body: data });
setIsSubmitted(true);
}, []);
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>
);

View File

@ -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>
</div>
<h3 className="font-bold text-foreground text-sm">Live Chat</h3>
<p className="text-sm text-muted-foreground">Available during business hours</p>
</div>
</button>
</div>
{/* 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;

View File

@ -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&apos;ll get back to you within 24 hours.
</p>
@ -279,5 +279,3 @@ export function PublicSupportView() {
</div>
);
}
export default PublicSupportView;

View 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 &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-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&apos;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
```

View 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`