Assist_Design/docs/plans/2026-03-04-landing-page-conversion-overhaul.md
barsa ee85426743 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.
2026-03-04 14:50:45 +09:00

29 KiB

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):

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):

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

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

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

{
  /* 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>;
{
  /* 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>;
{/* 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:

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:

// 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:

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

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

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:

{/* 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
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