931 lines
29 KiB
Markdown
931 lines
29 KiB
Markdown
|
|
# 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
|
||
|
|
```
|