+
+ {stats.map((stat, i) => (
+
+
+
+
+
+ {stat.kind === "animated" ? (
+
+ ) : (
+
+ {stat.text}
+
+ )}
+
{stat.label}
+
))}
diff --git a/apps/portal/src/features/landing-page/data/contact-subjects.ts b/apps/portal/src/features/landing-page/data/contact-subjects.ts
deleted file mode 100644
index 3ab74dcb..00000000
--- a/apps/portal/src/features/landing-page/data/contact-subjects.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-// =============================================================================
-// TYPES
-// =============================================================================
-
-export interface FormData {
- subject: string;
- name: string;
- email: string;
- phone: string;
- message: string;
-}
-
-export interface FormErrors {
- subject?: string;
- name?: string;
- email?: string;
- message?: string;
-}
-
-export interface FormTouched {
- subject?: boolean;
- name?: boolean;
- email?: boolean;
- phone?: boolean;
- message?: boolean;
-}
-
-// =============================================================================
-// DATA
-// =============================================================================
-
-export const CONTACT_SUBJECTS = [
- { value: "", label: "Select a topic*" },
- { value: "internet", label: "Internet Service Inquiry" },
- { value: "sim", label: "Phone/SIM Plan Inquiry" },
- { value: "vpn", label: "VPN Service Inquiry" },
- { value: "business", label: "Business Solutions" },
- { value: "support", label: "Technical Support" },
- { value: "billing", label: "Billing Question" },
- { value: "other", label: "Other" },
-];
-
-// =============================================================================
-// FORM VALIDATION
-// =============================================================================
-
-export function validateEmail(email: string): boolean {
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
-}
-
-export function validateForm(data: FormData): FormErrors {
- const errors: FormErrors = {};
-
- if (!data.subject) {
- errors.subject = "Please select a topic";
- }
- if (!data.name.trim()) {
- errors.name = "Name is required";
- }
- if (!data.email.trim()) {
- errors.email = "Email is required";
- } else if (!validateEmail(data.email)) {
- errors.email = "Please enter a valid email address";
- }
- if (!data.message.trim()) {
- errors.message = "Message is required";
- } else if (data.message.trim().length < 10) {
- errors.message = "Message must be at least 10 characters";
- }
-
- return errors;
-}
diff --git a/apps/portal/src/features/landing-page/data/index.ts b/apps/portal/src/features/landing-page/data/index.ts
index c70298a3..d0a51b05 100644
--- a/apps/portal/src/features/landing-page/data/index.ts
+++ b/apps/portal/src/features/landing-page/data/index.ts
@@ -2,6 +2,7 @@ export {
type ServiceCategory,
type ServiceItem,
type ConversionServiceCard,
+ type CarouselAccent,
type LandingServiceItem,
personalServices,
businessServices,
@@ -12,12 +13,3 @@ export {
mobileQuickServices,
landingServices,
} from "./services";
-
-export {
- type FormData,
- type FormErrors,
- type FormTouched,
- CONTACT_SUBJECTS,
- validateEmail,
- validateForm,
-} from "./contact-subjects";
diff --git a/apps/portal/src/features/landing-page/data/services.tsx b/apps/portal/src/features/landing-page/data/services.tsx
index a81ab478..96bfcbc6 100644
--- a/apps/portal/src/features/landing-page/data/services.tsx
+++ b/apps/portal/src/features/landing-page/data/services.tsx
@@ -17,6 +17,16 @@ import type { ServiceCardAccentColor } from "@/components/molecules";
export type ServiceCategory = "personal" | "business";
+export type CarouselAccent =
+ | "blue"
+ | "emerald"
+ | "violet"
+ | "amber"
+ | "indigo"
+ | "cyan"
+ | "rose"
+ | "slate";
+
export interface ServiceItem {
title: string;
icon: React.ReactNode;
@@ -27,6 +37,8 @@ export interface ConversionServiceCard {
title: string;
problemHook: string;
keyBenefit: string;
+ description: string;
+ accent: CarouselAccent;
badge?: string;
icon: React.ReactNode;
href: string;
@@ -98,6 +110,9 @@ export const personalConversionCards: ConversionServiceCard[] = [
title: "Internet Plans",
problemHook: "Need reliable internet?",
keyBenefit: "NTT Fiber up to 10Gbps",
+ description:
+ "High-speed NTT fiber with full English installation support. No Japanese paperwork — we handle everything for you.",
+ accent: "blue",
icon:
,
href: "/services/internet",
ctaLabel: "View Plans",
@@ -106,6 +121,9 @@ export const personalConversionCards: ConversionServiceCard[] = [
title: "Phone Plans",
problemHook: "Need a SIM card?",
keyBenefit: "Docomo network coverage",
+ description:
+ "SIM cards on Japan's best network. Foreign credit cards accepted, no hanko required. Get connected in days.",
+ accent: "emerald",
badge: "1st month free",
icon:
,
href: "/services/sim",
@@ -115,6 +133,9 @@ export const personalConversionCards: ConversionServiceCard[] = [
title: "VPN Service",
problemHook: "Missing shows from home?",
keyBenefit: "Stream US & UK content",
+ description:
+ "Stream your favorite shows from home. Pre-configured router — just plug in and watch US & UK content.",
+ accent: "violet",
icon:
,
href: "/services/vpn",
ctaLabel: "View Plans",
@@ -123,6 +144,9 @@ export const personalConversionCards: ConversionServiceCard[] = [
title: "Onsite Support",
problemHook: "Need hands-on help?",
keyBenefit: "English-speaking technicians",
+ description:
+ "English-speaking technicians at your door. Router setup, network troubleshooting, and device configuration.",
+ accent: "amber",
icon:
,
href: "/services/onsite",
ctaLabel: "Learn More",
@@ -134,6 +158,9 @@ export const businessConversionCards: ConversionServiceCard[] = [
title: "Office LAN Setup",
problemHook: "Setting up an office?",
keyBenefit: "Complete network infrastructure",
+ description:
+ "Complete network infrastructure for your Japan office. Professional installation with ongoing bilingual support.",
+ accent: "slate",
icon:
,
href: "/services/business",
ctaLabel: "Get a Quote",
@@ -142,6 +169,9 @@ export const businessConversionCards: ConversionServiceCard[] = [
title: "Tech Support",
problemHook: "Need ongoing IT help?",
keyBenefit: "Onsite & remote support",
+ description:
+ "Dedicated English-speaking IT team for your business. Onsite and remote support whenever you need it.",
+ accent: "amber",
icon:
,
href: "/services/onsite",
ctaLabel: "Get a Quote",
@@ -150,6 +180,9 @@ export const businessConversionCards: ConversionServiceCard[] = [
title: "Dedicated Internet",
problemHook: "Need guaranteed bandwidth?",
keyBenefit: "Enterprise-grade connectivity",
+ description:
+ "Enterprise-grade connectivity with guaranteed bandwidth and SLA. Built for businesses that can't afford downtime.",
+ accent: "indigo",
icon:
,
href: "/services/business",
ctaLabel: "Get a Quote",
@@ -158,6 +191,9 @@ export const businessConversionCards: ConversionServiceCard[] = [
title: "Data Center",
problemHook: "Need hosting in Japan?",
keyBenefit: "Secure, reliable infrastructure",
+ description:
+ "Secure, reliable hosting infrastructure in Japan. Colocation and managed services for your critical systems.",
+ accent: "cyan",
icon:
,
href: "/services/business",
ctaLabel: "Get a Quote",
@@ -166,6 +202,9 @@ export const businessConversionCards: ConversionServiceCard[] = [
title: "Website Services",
problemHook: "Need a web presence?",
keyBenefit: "Construction & maintenance",
+ description:
+ "Professional website construction and ongoing maintenance. Bilingual design that connects with your audience.",
+ accent: "rose",
icon:
,
href: "/services/business",
ctaLabel: "Get a Quote",
diff --git a/apps/portal/src/features/landing-page/hooks/index.ts b/apps/portal/src/features/landing-page/hooks/index.ts
index 7ac6519e..85e5d8ee 100644
--- a/apps/portal/src/features/landing-page/hooks/index.ts
+++ b/apps/portal/src/features/landing-page/hooks/index.ts
@@ -1,3 +1,2 @@
export { useInView } from "./useInView";
-export { useContactForm } from "./useContactForm";
export { useStickyCta } from "./useStickyCta";
diff --git a/apps/portal/src/features/landing-page/hooks/useContactForm.ts b/apps/portal/src/features/landing-page/hooks/useContactForm.ts
deleted file mode 100644
index 265e482c..00000000
--- a/apps/portal/src/features/landing-page/hooks/useContactForm.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { useCallback, useState } from "react";
-
-import type { FormData, FormErrors, FormTouched } from "@/features/landing-page/data";
-import { validateForm } from "@/features/landing-page/data";
-
-/**
- * useContactForm - Manages contact form state, validation, and submission.
- *
- * Encapsulates:
- * - Form field values (formData)
- * - Validation errors (formErrors)
- * - Touch tracking for blur-based validation (formTouched)
- * - Submission state (isSubmitting, submitStatus)
- * - Input change, blur, and submit handlers
- *
- * Note: Sticky CTA visibility has been extracted to `useStickyCta`.
- */
-export function useContactForm() {
- // Form state
- const [formData, setFormData] = useState
({
- subject: "",
- name: "",
- email: "",
- phone: "",
- message: "",
- });
- const [formErrors, setFormErrors] = useState({});
- const [formTouched, setFormTouched] = useState({});
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [submitStatus, setSubmitStatus] = useState<"idle" | "success" | "error">("idle");
-
- // ---------------------------------------------------------------------------
- // Handlers
- // ---------------------------------------------------------------------------
- const handleInputChange = useCallback(
- (e: React.ChangeEvent) => {
- const { name, value } = e.target;
- setFormData(prev => ({ ...prev, [name]: value }));
-
- // Clear error when user starts typing
- if (formErrors[name as keyof FormErrors]) {
- setFormErrors(prev => ({ ...prev, [name]: undefined }));
- }
- },
- [formErrors]
- );
-
- const handleInputBlur = useCallback(
- (e: React.FocusEvent) => {
- const { name } = e.target;
- setFormTouched(prev => ({ ...prev, [name]: true }));
-
- // Validate single field on blur
- const errors = validateForm(formData);
- if (errors[name as keyof FormErrors]) {
- setFormErrors(prev => ({ ...prev, [name]: errors[name as keyof FormErrors] }));
- }
- },
- [formData]
- );
-
- const handleSubmit = useCallback(
- async (e: React.FormEvent) => {
- e.preventDefault();
-
- // Validate all fields
- const errors = validateForm(formData);
- setFormErrors(errors);
- setFormTouched({ subject: true, name: true, email: true, message: true });
-
- if (Object.keys(errors).length > 0) {
- return;
- }
-
- setIsSubmitting(true);
- setSubmitStatus("idle");
-
- try {
- // Simulate API call - replace with actual endpoint
- await new Promise(resolve => {
- setTimeout(resolve, 1500);
- });
-
- // Success
- setSubmitStatus("success");
- setFormData({ subject: "", name: "", email: "", phone: "", message: "" });
- setFormTouched({});
- setFormErrors({});
-
- // Reset success message after 5 seconds
- setTimeout(() => setSubmitStatus("idle"), 5000);
- } catch {
- setSubmitStatus("error");
- } finally {
- setIsSubmitting(false);
- }
- },
- [formData]
- );
-
- return {
- formData,
- formErrors,
- formTouched,
- isSubmitting,
- submitStatus,
- handleInputChange,
- handleInputBlur,
- handleSubmit,
- };
-}
diff --git a/apps/portal/src/features/services/components/base/HowItWorks.tsx b/apps/portal/src/features/services/components/base/HowItWorks.tsx
index 834392f6..de72cb88 100644
--- a/apps/portal/src/features/services/components/base/HowItWorks.tsx
+++ b/apps/portal/src/features/services/components/base/HowItWorks.tsx
@@ -35,7 +35,9 @@ export function HowItWorks({
{eyebrow}
- {title}
+
+ {title}
+
{/* Steps Container */}
diff --git a/apps/portal/src/features/services/components/base/ServiceCTA.tsx b/apps/portal/src/features/services/components/base/ServiceCTA.tsx
index f63f923d..c2558553 100644
--- a/apps/portal/src/features/services/components/base/ServiceCTA.tsx
+++ b/apps/portal/src/features/services/components/base/ServiceCTA.tsx
@@ -64,7 +64,9 @@ export function ServiceCTA({