refactor: update landing page components and styles
- Removed obsolete components such as AnimatedBackground, FloatingGlassCard, TrustBadge, TrustIndicators, and ValuePropCard to streamline the landing page. - Enhanced existing components like CTABanner and HeroSection with improved accessibility and styling. - Updated global CSS to introduce new line-height tokens and improved typography. - Refactored the PublicContactView to focus on a streamlined contact form and sidebar information. - Improved the ServicesGrid component to utilize a new data structure for landing services. - Enhanced button components with new variants for better UI consistency.
This commit is contained in:
parent
0523bf80a8
commit
5a66adb7e6
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { PublicContactView } from "@/features/support/views/PublicContactView";
|
||||
import { PublicContactView } from "@/features/support/views";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Contact Us - English Support for Expats | Assist Solutions",
|
||||
|
||||
32
apps/portal/src/app/(public)/(site)/support/page.tsx
Normal file
32
apps/portal/src/app/(public)/(site)/support/page.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Public Support Page
|
||||
*
|
||||
* Self-service support hub with knowledge base categories,
|
||||
* remote support tools, FAQ, and contact form fallback.
|
||||
*/
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { PublicSupportView } from "@/features/support/views";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Support - Self-Service Help Center | Assist Solutions",
|
||||
description:
|
||||
"Find answers to common questions, download remote support tools, or contact our English-speaking team. No Japanese required.",
|
||||
keywords: [
|
||||
"IT support Japan",
|
||||
"English tech support Tokyo",
|
||||
"remote support Japan",
|
||||
"expat tech help",
|
||||
"Assist Solutions support",
|
||||
],
|
||||
openGraph: {
|
||||
title: "Support - Help Center | Assist Solutions",
|
||||
description:
|
||||
"Self-service support hub for expats in Japan. FAQ, remote support tools, and direct contact options.",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function SupportPage() {
|
||||
return <PublicSupportView />;
|
||||
}
|
||||
@ -318,7 +318,7 @@
|
||||
--color-glass-border: var(--glass-border);
|
||||
|
||||
/* Line-height tokens */
|
||||
--leading-display: 1.1;
|
||||
--leading-display: var(--cp-leading-display);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@ -20,6 +20,8 @@ const buttonVariants = cva(
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]",
|
||||
ghost: "text-foreground hover:bg-muted",
|
||||
link: "underline-offset-4 hover:underline text-primary",
|
||||
pill: "rounded-full bg-primary text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 hover:-translate-y-0.5",
|
||||
pillOutline: "rounded-full border border-border bg-card text-primary hover:bg-primary/5",
|
||||
},
|
||||
size: {
|
||||
default: "h-11 py-2.5 px-4",
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
import { cn } from "@/shared/utils";
|
||||
|
||||
interface AnimatedBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mesh gradient background with floating geometric shapes
|
||||
*/
|
||||
export function AnimatedBackground({ className }: AnimatedBackgroundProps) {
|
||||
return (
|
||||
<div className={cn("absolute inset-0 -z-10 overflow-hidden", className)}>
|
||||
{/* Mesh gradient */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0",
|
||||
"bg-[radial-gradient(ellipse_at_20%_30%,_oklch(0.72_0.12_260_/_0.12)_0%,_transparent_50%),",
|
||||
"radial-gradient(ellipse_at_80%_20%,_oklch(0.72_0.14_290_/_0.08)_0%,_transparent_50%),",
|
||||
"radial-gradient(ellipse_at_60%_80%,_oklch(0.75_0.1_200_/_0.06)_0%,_transparent_50%)]"
|
||||
)}
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(ellipse at 20% 30%, oklch(0.72 0.12 260 / 0.12) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 20%, oklch(0.72 0.14 290 / 0.08) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 60% 80%, oklch(0.75 0.1 200 / 0.06) 0%, transparent 50%)
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Floating shapes */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-1/4 left-1/4 w-64 h-64 rounded-full",
|
||||
"bg-gradient-to-br from-primary/5 to-transparent",
|
||||
"cp-float-slow blur-3xl"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-1/2 right-1/4 w-48 h-48 rounded-full",
|
||||
"bg-gradient-to-br from-accent-gradient/5 to-transparent",
|
||||
"cp-float-delayed blur-3xl"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-1/4 left-1/3 w-32 h-32 rounded-full",
|
||||
"bg-gradient-to-br from-primary/8 to-transparent",
|
||||
"cp-float blur-2xl"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Subtle grid pattern overlay */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.02]"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, currentColor 1px, transparent 1px),
|
||||
linear-gradient(to bottom, currentColor 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "64px 64px",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,10 +1,15 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Phone } from "lucide-react";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
|
||||
const PHONE_NUMBER = "03-5812-1050";
|
||||
|
||||
export function CTABanner() {
|
||||
return (
|
||||
<section 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 py-14 sm:py-16 text-center">
|
||||
<section
|
||||
aria-label="Get in touch"
|
||||
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?
|
||||
</h2>
|
||||
@ -13,26 +18,29 @@ export function CTABanner() {
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex items-center justify-center gap-2 text-lg font-semibold text-primary">
|
||||
<Phone className="h-5 w-5" />
|
||||
<a href="tel:03-5812-1050" className="hover:underline">
|
||||
03-5812-1050
|
||||
<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">
|
||||
<Link
|
||||
<Button
|
||||
as="a"
|
||||
href="/services"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full px-8 py-3 text-base font-semibold bg-primary text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 transition-transform hover:-translate-y-0.5"
|
||||
variant="pill"
|
||||
size="lg"
|
||||
rightIcon={<ArrowRight className="h-5 w-5" />}
|
||||
>
|
||||
Browse Services
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full px-8 py-3 text-base font-semibold border border-border bg-card text-primary hover:bg-primary/5 transition-colors"
|
||||
>
|
||||
</Button>
|
||||
<Button as="a" href="/contact" variant="pillOutline" size="lg">
|
||||
Contact Us
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import { cn } from "@/shared/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
interface FloatingGlassCardProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
className?: string;
|
||||
/** Accent color for icon */
|
||||
accentColor?: string;
|
||||
/** Optional inline styles for animation delays */
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const accentColorMap: Record<string, string> = {
|
||||
blue: "text-blue-500",
|
||||
green: "text-green-500",
|
||||
purple: "text-purple-500",
|
||||
amber: "text-amber-500",
|
||||
rose: "text-rose-500",
|
||||
cyan: "text-cyan-500",
|
||||
primary: "text-primary",
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorative floating glass card for hero section
|
||||
*/
|
||||
export function FloatingGlassCard({
|
||||
icon: Icon,
|
||||
title,
|
||||
subtitle,
|
||||
className,
|
||||
accentColor = "primary",
|
||||
style,
|
||||
}: FloatingGlassCardProps) {
|
||||
const colorClass = accentColorMap[accentColor] || accentColorMap["primary"];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("cp-glass-card px-5 py-4 min-w-[200px]", "shadow-xl shadow-black/5", className)}
|
||||
style={style}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-lg flex items-center justify-center",
|
||||
"bg-background/50",
|
||||
colorClass
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-foreground text-sm">{title}</p>
|
||||
<p className="text-xs text-muted-foreground">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { useInView } from "@/features/landing-page/hooks";
|
||||
|
||||
interface HeroSectionProps {
|
||||
@ -14,9 +15,10 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
|
||||
return (
|
||||
<section
|
||||
ref={heroRef as React.RefObject<HTMLElement>}
|
||||
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-16 sm:py-20 lg:py-24 overflow-hidden transition-all duration-700 ${
|
||||
className={cn(
|
||||
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-16 sm:py-20 lg:py-24 overflow-hidden transition-all duration-700",
|
||||
heroInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
}`}
|
||||
)}
|
||||
>
|
||||
{/* Gradient Background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-surface-sunken via-background to-info-bg/80" />
|
||||
@ -24,6 +26,7 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
|
||||
{/* Dot Grid Pattern Overlay */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle at center, oklch(0.65 0.05 234.4 / 0.15) 1px, transparent 1px)`,
|
||||
backgroundSize: "24px 24px",
|
||||
@ -33,6 +36,7 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
|
||||
{/* Subtle gradient accent in corner */}
|
||||
<div
|
||||
className="absolute -top-32 -right-32 w-96 h-96 rounded-full pointer-events-none"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
background: "radial-gradient(circle, oklch(0.85 0.08 200 / 0.3) 0%, transparent 70%)",
|
||||
}}
|
||||
@ -40,30 +44,29 @@ 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">English IT Support</span>
|
||||
<span className="block text-primary mt-2">for Expats in Japan</span>
|
||||
<span className="block">A One Stop Solution</span>
|
||||
<span className="block text-primary mt-2">for Your IT Needs</span>
|
||||
</h1>
|
||||
<p className="mt-6 text-base sm:text-lg text-muted-foreground leading-relaxed font-semibold max-w-2xl mx-auto">
|
||||
No Japanese required. Get reliable internet, mobile, and VPN services with full English
|
||||
support. Serving expats and international businesses for over 20 years.
|
||||
From internet and mobile to VPN and on-site tech support — we handle it all in English so
|
||||
you don't have to.
|
||||
</p>
|
||||
<div
|
||||
ref={heroCTARef}
|
||||
className="mt-8 flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4"
|
||||
>
|
||||
<Link
|
||||
<Button
|
||||
as="a"
|
||||
href="/services"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full px-6 sm:px-8 py-3 text-base font-semibold bg-primary text-primary-foreground hover:bg-primary/90 shadow-md shadow-primary/20 transition-transform hover:-translate-y-0.5"
|
||||
variant="pill"
|
||||
size="lg"
|
||||
rightIcon={<ArrowRight className="h-5 w-5" />}
|
||||
>
|
||||
Browse Services
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full px-6 sm:px-8 py-3 text-base font-semibold border border-border bg-card text-primary hover:bg-primary/5 transition-colors"
|
||||
>
|
||||
</Button>
|
||||
<Button as="a" href="/contact" variant="pillOutline" size="lg">
|
||||
Need Assistance?
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"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";
|
||||
|
||||
@ -10,9 +11,10 @@ export function ServicesGrid() {
|
||||
return (
|
||||
<section
|
||||
ref={ref as React.RefObject<HTMLElement>}
|
||||
className={`py-14 sm:py-16 transition-all duration-700 ${
|
||||
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">
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import { cn } from "@/shared/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
interface TrustBadgeProps {
|
||||
icon?: LucideIcon;
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trust badge for hero sections - establishes credibility
|
||||
*/
|
||||
export function TrustBadge({ icon: Icon, text, className }: TrustBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-full",
|
||||
"bg-primary/8 border border-primary/15 px-4 py-2",
|
||||
"text-sm font-medium text-primary",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon className="h-4 w-4" />}
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
import { cn } from "@/shared/utils";
|
||||
import { Users, Calendar, Shield } from "lucide-react";
|
||||
|
||||
interface TrustIndicatorsProps {
|
||||
className?: string;
|
||||
variant?: "horizontal" | "compact";
|
||||
}
|
||||
|
||||
const stats = [
|
||||
{
|
||||
icon: Calendar,
|
||||
value: "20+",
|
||||
label: "Years in Japan",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
value: "10,000+",
|
||||
label: "Customers Served",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
value: "NTT",
|
||||
label: "Authorized Partner",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Trust indicators showing company credibility metrics
|
||||
*/
|
||||
export function TrustIndicators({ className, variant = "horizontal" }: TrustIndicatorsProps) {
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<div className={cn("flex flex-wrap items-center gap-x-6 gap-y-2", className)}>
|
||||
{stats.map((stat, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<stat.icon className="h-4 w-4 text-primary/70" />
|
||||
<span className="font-semibold text-foreground">{stat.value}</span>
|
||||
<span>{stat.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col sm:flex-row items-start sm:items-center gap-6 sm:gap-8",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{stats.map((stat, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/8">
|
||||
<stat.icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-bold text-foreground">{stat.value}</div>
|
||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -9,7 +9,10 @@ const trustItems = [
|
||||
|
||||
export function TrustStrip() {
|
||||
return (
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/50 border-y border-border/40">
|
||||
<section
|
||||
aria-label="Trust highlights"
|
||||
className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/50 border-y border-border/40"
|
||||
>
|
||||
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-14 py-5">
|
||||
<div className="grid grid-cols-2 gap-4 sm:flex sm:justify-between">
|
||||
{trustItems.map(({ icon: Icon, label }) => (
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
import { cn } from "@/shared/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
interface ValuePropCardProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Glass morphism value proposition card with gradient border on hover
|
||||
*/
|
||||
export function ValuePropCard({ icon: Icon, title, description, className }: ValuePropCardProps) {
|
||||
return (
|
||||
<div className={cn("group relative", className)}>
|
||||
{/* Gradient border wrapper - visible on hover */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -inset-0.5 rounded-2xl",
|
||||
"bg-gradient-to-r from-primary/50 via-accent-gradient/50 to-primary/50",
|
||||
"opacity-0 group-hover:opacity-100 blur-sm",
|
||||
"transition-opacity duration-[var(--cp-duration-slowest)]"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Card */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex flex-col items-center text-center p-8",
|
||||
"bg-card/80 dark:bg-card/60 backdrop-blur-sm",
|
||||
"border border-border/50 rounded-2xl",
|
||||
"transition-all duration-[var(--cp-duration-normal)]",
|
||||
"group-hover:-translate-y-1"
|
||||
)}
|
||||
>
|
||||
{/* Icon with animated background */}
|
||||
<div className="relative h-16 w-16 mb-6">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-xl",
|
||||
"bg-gradient-to-br from-primary/20 to-primary/5",
|
||||
"transition-transform duration-[var(--cp-duration-slow)]",
|
||||
"group-hover:scale-110"
|
||||
)}
|
||||
/>
|
||||
<div className="relative h-full w-full flex items-center justify-center text-primary">
|
||||
<Icon
|
||||
className={cn(
|
||||
"h-8 w-8",
|
||||
"transition-transform duration-[var(--cp-duration-slow)]",
|
||||
"group-hover:scale-110"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h3 className="text-xl font-bold text-foreground mb-3 font-display">{title}</h3>
|
||||
<p className="text-muted-foreground leading-relaxed max-w-xs">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, BadgeCheck } from "lucide-react";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { useInView } from "@/features/landing-page/hooks";
|
||||
|
||||
const trustPoints = [
|
||||
@ -17,9 +18,10 @@ export function WhyUsSection() {
|
||||
return (
|
||||
<section
|
||||
ref={ref as React.RefObject<HTMLElement>}
|
||||
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-background transition-all duration-700 ${
|
||||
className={cn(
|
||||
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-background 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 py-14 sm:py-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-14 items-center">
|
||||
|
||||
@ -4,12 +4,3 @@ export { TrustStrip } from "./TrustStrip";
|
||||
export { ServicesGrid } from "./ServicesGrid";
|
||||
export { WhyUsSection } from "./WhyUsSection";
|
||||
export { CTABanner } from "./CTABanner";
|
||||
|
||||
// Trust indicators
|
||||
export { TrustBadge } from "./TrustBadge";
|
||||
export { TrustIndicators } from "./TrustIndicators";
|
||||
|
||||
// Decorative/visual components (kept for potential future use)
|
||||
export { ValuePropCard } from "./ValuePropCard";
|
||||
export { FloatingGlassCard } from "./FloatingGlassCard";
|
||||
export { AnimatedBackground } from "./AnimatedBackground";
|
||||
|
||||
@ -140,48 +140,20 @@ export const supportDownloads = [
|
||||
export const mobileQuickServices = personalServices.slice(0, 3);
|
||||
|
||||
// =============================================================================
|
||||
// LANDING PAGE SERVICES (for ServicesGrid)
|
||||
// LANDING PAGE SERVICES (for ServicesGrid — derived from `services`)
|
||||
// =============================================================================
|
||||
|
||||
export interface LandingServiceItem {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
href: string;
|
||||
export type LandingServiceItem = (typeof services)[number] & {
|
||||
accentColor: ServiceCardAccentColor;
|
||||
}
|
||||
};
|
||||
|
||||
export const landingServices: LandingServiceItem[] = [
|
||||
{
|
||||
title: "Internet Plans",
|
||||
description:
|
||||
"High-speed NTT fiber with English installation support. No Japanese paperwork needed.",
|
||||
icon: <Wifi className="h-6 w-6" />,
|
||||
href: "/services/internet",
|
||||
accentColor: "blue",
|
||||
},
|
||||
{
|
||||
title: "Phone Plans",
|
||||
description:
|
||||
"SIM cards on Japan's best network. Foreign credit cards accepted, no hanko required.",
|
||||
icon: <Smartphone className="h-6 w-6" />,
|
||||
href: "/services/sim",
|
||||
accentColor: "green",
|
||||
},
|
||||
{
|
||||
title: "VPN Service",
|
||||
description:
|
||||
"Stream your favorite shows from home. Pre-configured router, just plug in and watch.",
|
||||
icon: <Lock className="h-6 w-6" />,
|
||||
href: "/services/vpn",
|
||||
accentColor: "purple",
|
||||
},
|
||||
{
|
||||
title: "Business Solutions",
|
||||
description:
|
||||
"Enterprise IT for international companies. Dedicated internet, networks, and bilingual support.",
|
||||
icon: <Building2 className="h-6 w-6" />,
|
||||
href: "/services/business",
|
||||
accentColor: "orange",
|
||||
},
|
||||
];
|
||||
const landingAccentMap: Record<string, ServiceCardAccentColor> = {
|
||||
"/services/internet": "blue",
|
||||
"/services/sim": "green",
|
||||
"/services/vpn": "purple",
|
||||
"/services/business": "orange",
|
||||
};
|
||||
|
||||
export const landingServices: LandingServiceItem[] = services
|
||||
.filter(s => s.href in landingAccentMap)
|
||||
.map(s => ({ ...s, accentColor: landingAccentMap[s.href]! }));
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import type { FormData, FormErrors, FormTouched } from "@/features/landing-page/data";
|
||||
import { validateForm } from "@/features/landing-page/data";
|
||||
@ -11,8 +11,9 @@ import { validateForm } from "@/features/landing-page/data";
|
||||
* - Validation errors (formErrors)
|
||||
* - Touch tracking for blur-based validation (formTouched)
|
||||
* - Submission state (isSubmitting, submitStatus)
|
||||
* - Sticky CTA visibility (showStickyCTA) tied to hero CTA intersection
|
||||
* - Input change, blur, and submit handlers
|
||||
*
|
||||
* Note: Sticky CTA visibility has been extracted to `useStickyCta`.
|
||||
*/
|
||||
export function useContactForm() {
|
||||
// Form state
|
||||
@ -28,30 +29,6 @@ export function useContactForm() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<"idle" | "success" | "error">("idle");
|
||||
|
||||
// Hero CTA visibility for sticky mobile CTA
|
||||
const heroCTARef = useRef<HTMLDivElement>(null);
|
||||
const [showStickyCTA, setShowStickyCTA] = useState(false);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sticky CTA observer
|
||||
// ---------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const ctaElement = heroCTARef.current;
|
||||
if (!ctaElement) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry) {
|
||||
setShowStickyCTA(!entry.isIntersecting);
|
||||
}
|
||||
},
|
||||
{ threshold: 0 }
|
||||
);
|
||||
|
||||
observer.observe(ctaElement);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -127,8 +104,6 @@ export function useContactForm() {
|
||||
formTouched,
|
||||
isSubmitting,
|
||||
submitStatus,
|
||||
showStickyCTA,
|
||||
heroCTARef,
|
||||
handleInputChange,
|
||||
handleInputBlur,
|
||||
handleSubmit,
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const DEFAULT_OPTIONS: IntersectionObserverInit = {};
|
||||
|
||||
/**
|
||||
* useInView - Intersection Observer hook for scroll-triggered animations
|
||||
* Returns a ref and boolean indicating if element is in viewport.
|
||||
* Once the element becomes visible, it stays marked as "in view" (trigger once).
|
||||
*/
|
||||
export function useInView(options: IntersectionObserverInit = {}) {
|
||||
export function useInView(options: IntersectionObserverInit = DEFAULT_OPTIONS) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const [isInView, setIsInView] = useState(false);
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
|
||||
@ -24,7 +24,7 @@ export function PublicLandingLoadingView() {
|
||||
{/* TrustStrip Skeleton */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-surface-sunken/50 border-y border-border/40">
|
||||
<div className="mx-auto max-w-6xl px-6 sm:px-10 lg:px-14 py-5">
|
||||
<div className="flex justify-between">
|
||||
<div className="grid grid-cols-2 gap-4 sm:flex sm:justify-between">
|
||||
{Array.from({ length: 4 }).map((_, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2.5">
|
||||
<Skeleton className="h-5 w-5 rounded-full" />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import { useStickyCta } from "@/features/landing-page/hooks";
|
||||
import {
|
||||
HeroSection,
|
||||
@ -25,13 +25,16 @@ export function PublicLandingView() {
|
||||
{/* 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">
|
||||
<Link
|
||||
<Button
|
||||
as="a"
|
||||
href="/services"
|
||||
className="flex items-center justify-center gap-2 w-full rounded-full bg-primary px-6 py-3 text-base font-semibold text-primary-foreground shadow-lg"
|
||||
variant="pill"
|
||||
size="lg"
|
||||
rightIcon={<ArrowRight className="h-5 w-5" />}
|
||||
className="w-full shadow-lg"
|
||||
>
|
||||
Browse Services
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -35,9 +35,7 @@ export function HowItWorks({
|
||||
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-2">
|
||||
{eyebrow}
|
||||
</p>
|
||||
<h2 className="text-display-sm font-display font-semibold leading-tight text-foreground">
|
||||
{title}
|
||||
</h2>
|
||||
<h2 className="text-section-heading text-foreground">{title}</h2>
|
||||
</div>
|
||||
|
||||
{/* Steps Container */}
|
||||
|
||||
@ -64,9 +64,7 @@ export function ServiceCTA({
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<h2 className="text-display-sm font-display font-semibold leading-tight text-foreground mb-3">
|
||||
{headline}
|
||||
</h2>
|
||||
<h2 className="text-section-heading text-foreground mb-3">{headline}</h2>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-base text-muted-foreground mb-6 max-w-md mx-auto">{description}</p>
|
||||
|
||||
@ -84,9 +84,7 @@ export function ServiceFAQ({
|
||||
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-2">
|
||||
{eyebrow}
|
||||
</p>
|
||||
<h2 className="text-display-sm font-display font-semibold leading-tight text-foreground">
|
||||
{title}
|
||||
</h2>
|
||||
<h2 className="text-section-heading text-foreground">{title}</h2>
|
||||
</div>
|
||||
|
||||
{/* FAQ Container */}
|
||||
|
||||
@ -19,7 +19,7 @@ interface ServiceHighlightsProps {
|
||||
function HighlightItem({ icon, title, description, highlight }: HighlightFeature) {
|
||||
return (
|
||||
<div className="group relative flex items-start gap-3.5 p-4 rounded-xl border border-border/40 hover:bg-muted/40 transition-colors duration-200">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary flex-shrink-0 mt-0.5 [&>svg]:h-5 [&>svg]:w-5">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary flex-shrink-0 mt-0.5">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@ -84,8 +84,8 @@ export function ServiceHighlights({ features, className = "" }: ServiceHighlight
|
||||
const handleScroll = () => {
|
||||
const scrollLeft = container.scrollLeft;
|
||||
const itemWidth = 280 + 12; // card width + gap
|
||||
const newIndex = Math.round(scrollLeft / itemWidth);
|
||||
setActiveIndex(Math.min(newIndex, features.length - 1));
|
||||
const newIndex = Math.min(Math.round(scrollLeft / itemWidth), features.length - 1);
|
||||
setActiveIndex(prev => (prev === newIndex ? prev : newIndex));
|
||||
};
|
||||
|
||||
container.addEventListener("scroll", handleScroll, { passive: true });
|
||||
|
||||
@ -7,10 +7,7 @@ import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import type { VpnCatalogProduct } from "@customer-portal/domain/services";
|
||||
import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink";
|
||||
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
||||
import {
|
||||
ServiceHighlights,
|
||||
type HighlightFeature,
|
||||
} from "@/features/services/components/base/ServiceHighlights";
|
||||
import { ServiceHighlights } from "@/features/services/components/base/ServiceHighlights";
|
||||
import { HowItWorks, type HowItWorksStep } from "@/features/services/components/base/HowItWorks";
|
||||
import { ServiceCTA } from "@/features/services/components/base/ServiceCTA";
|
||||
import { ServiceFAQ, type FAQItem } from "@/features/services/components/base/ServiceFAQ";
|
||||
@ -87,9 +84,6 @@ export function VpnPlansContent({
|
||||
}: VpnPlansContentProps) {
|
||||
const servicesBasePath = useServicesBasePath();
|
||||
|
||||
// Convert VPN_FEATURES to the HighlightFeature type expected by ServiceHighlights
|
||||
const vpnFeatures: HighlightFeature[] = VPN_FEATURES;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 pb-16 pt-8">
|
||||
@ -157,7 +151,7 @@ export function VpnPlansContent({
|
||||
</span>
|
||||
</div>
|
||||
<h1
|
||||
className="text-display-md md:text-display-lg font-display font-semibold md:font-bold leading-tight md:leading-display text-foreground animate-in fade-in slide-in-from-bottom-6 duration-700"
|
||||
className="text-display-md md:text-display-lg font-display font-bold leading-display text-foreground animate-in fade-in slide-in-from-bottom-6 duration-700"
|
||||
style={{ animationDelay: "100ms" }}
|
||||
>
|
||||
Stream Content from Abroad
|
||||
@ -194,7 +188,7 @@ export function VpnPlansContent({
|
||||
|
||||
{/* Service Highlights */}
|
||||
<section>
|
||||
<ServiceHighlights features={vpnFeatures} />
|
||||
<ServiceHighlights features={VPN_FEATURES} />
|
||||
</section>
|
||||
|
||||
{/* Plans Section */}
|
||||
@ -208,9 +202,7 @@ export function VpnPlansContent({
|
||||
<p className="text-sm font-semibold text-primary uppercase tracking-wider mb-2">
|
||||
Choose Your Region
|
||||
</p>
|
||||
<h2 className="text-display-sm font-display font-semibold leading-tight text-foreground">
|
||||
Available Plans
|
||||
</h2>
|
||||
<h2 className="text-section-heading text-foreground">Available Plans</h2>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Select one region per router rental
|
||||
</p>
|
||||
|
||||
180
apps/portal/src/features/support/components/ContactForm.tsx
Normal file
180
apps/portal/src/features/support/components/ContactForm.tsx
Normal file
@ -0,0 +1,180 @@
|
||||
"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";
|
||||
import { useZodForm } from "@/shared/hooks";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
import {
|
||||
publicContactRequestSchema,
|
||||
type PublicContactRequest,
|
||||
} from "@customer-portal/domain/support";
|
||||
import { apiClient, ApiError, isApiError } from "@/core/api";
|
||||
import { cn } from "@/shared/utils";
|
||||
|
||||
interface ContactFormProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ContactForm({ className }: ContactFormProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = useCallback(async (data: PublicContactRequest) => {
|
||||
setSubmitError(null);
|
||||
|
||||
try {
|
||||
await apiClient.POST("/api/support/contact", { body: data });
|
||||
setIsSubmitted(true);
|
||||
} catch (error) {
|
||||
if (isApiError(error)) {
|
||||
setSubmitError(error.message || "Failed to send message");
|
||||
return;
|
||||
}
|
||||
if (error instanceof ApiError) {
|
||||
setSubmitError(error.message || "Failed to send message");
|
||||
return;
|
||||
}
|
||||
setSubmitError(error instanceof Error ? error.message : "Failed to send message");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const form = useZodForm<PublicContactRequest>({
|
||||
schema: publicContactRequestSchema,
|
||||
initialValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
},
|
||||
onSubmit: handleSubmit,
|
||||
});
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className={cn("text-center py-12", className)}>
|
||||
<div className="w-16 h-16 bg-success/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="h-8 w-8 text-success" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-foreground mb-2">Message Sent!</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Thank you for contacting us. We'll get back to you within 24 hours.
|
||||
</p>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Button as="a" href="/" variant="outline" size="sm">
|
||||
Back to Home
|
||||
</Button>
|
||||
<Button as="a" href="/services" size="sm">
|
||||
Browse Services
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("bg-white rounded-2xl border border-border/60 p-6", className)}>
|
||||
{submitError && (
|
||||
<AlertBanner variant="error" title="Error" className="mb-6">
|
||||
{submitError}
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
<form onSubmit={event => void form.handleSubmit(event)} className="space-y-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<FormField
|
||||
label="Name"
|
||||
error={form.touched["name"] ? form.errors["name"] : undefined}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
value={form.values.name}
|
||||
onChange={e => form.setValue("name", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("name")}
|
||||
placeholder="Your name"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Email"
|
||||
error={form.touched["email"] ? form.errors["email"] : undefined}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="email"
|
||||
value={form.values.email}
|
||||
onChange={e => form.setValue("email", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("email")}
|
||||
placeholder="your@email.com"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<FormField label="Phone" error={form.touched["phone"] ? form.errors["phone"] : undefined}>
|
||||
<Input
|
||||
value={form.values.phone ?? ""}
|
||||
onChange={e => form.setValue("phone", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("phone")}
|
||||
placeholder="+81 90-1234-5678"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Subject"
|
||||
error={form.touched["subject"] ? form.errors["subject"] : undefined}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
value={form.values.subject}
|
||||
onChange={e => form.setValue("subject", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("subject")}
|
||||
placeholder="How can we help?"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
label="Message"
|
||||
error={form.touched["message"] ? form.errors["message"] : undefined}
|
||||
required
|
||||
>
|
||||
<textarea
|
||||
className="flex min-h-[120px] w-full rounded-lg border border-input bg-muted/20 px-4 py-3 text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:border-transparent disabled:cursor-not-allowed disabled:opacity-50 transition-colors resize-y text-sm"
|
||||
value={form.values.message}
|
||||
onChange={e => form.setValue("message", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("message")}
|
||||
placeholder="Tell us more about your inquiry..."
|
||||
rows={4}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={form.isSubmitting}
|
||||
isLoading={form.isSubmitting}
|
||||
loadingText="Sending..."
|
||||
>
|
||||
Send Message
|
||||
</Button>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
apps/portal/src/features/support/components/index.ts
Normal file
1
apps/portal/src/features/support/components/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ContactForm } from "./ContactForm";
|
||||
@ -1,170 +1,67 @@
|
||||
"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";
|
||||
import { useZodForm } from "@/shared/hooks";
|
||||
import {
|
||||
Mail,
|
||||
CheckCircle,
|
||||
MapPin,
|
||||
Phone,
|
||||
MessageSquare,
|
||||
ChevronDown,
|
||||
Clock,
|
||||
HelpCircle,
|
||||
Send,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
publicContactRequestSchema,
|
||||
type PublicContactRequest,
|
||||
} from "@customer-portal/domain/support";
|
||||
import { apiClient, ApiError, isApiError } from "@/core/api";
|
||||
import { cn } from "@/shared/utils";
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{
|
||||
question: "Sample Question 1?",
|
||||
answer:
|
||||
"This is a sample answer for frequently asked question 1. Replace this with actual content when available.",
|
||||
},
|
||||
{
|
||||
question: "Sample Question 2?",
|
||||
answer:
|
||||
"This is a sample answer for frequently asked question 2. Replace this with actual content when available.",
|
||||
},
|
||||
{
|
||||
question: "Sample Question 3?",
|
||||
answer:
|
||||
"This is a sample answer for frequently asked question 3. Replace this with actual content when available.",
|
||||
},
|
||||
{
|
||||
question: "Sample Question 4?",
|
||||
answer:
|
||||
"This is a sample answer for frequently asked question 4. Replace this with actual content when available.",
|
||||
},
|
||||
{
|
||||
question: "Sample Question 5?",
|
||||
answer:
|
||||
"This is a sample answer for frequently asked question 5. Replace this with actual content when available.",
|
||||
},
|
||||
];
|
||||
import { Mail, MapPin, Phone, MessageSquare, Clock, Send, ExternalLink } from "lucide-react";
|
||||
import { ContactForm } from "@/features/support/components";
|
||||
|
||||
/**
|
||||
* PublicContactView - Combined Support & Contact page
|
||||
* PublicContactView - Focused contact page with form + sidebar info
|
||||
*/
|
||||
export function PublicContactView() {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [expandedFaq, setExpandedFaq] = useState<number | null>(null);
|
||||
|
||||
const handleSubmit = useCallback(async (data: PublicContactRequest) => {
|
||||
setSubmitError(null);
|
||||
|
||||
try {
|
||||
await apiClient.POST("/api/support/contact", { body: data });
|
||||
setIsSubmitted(true);
|
||||
} catch (error) {
|
||||
if (isApiError(error)) {
|
||||
setSubmitError(error.message || "Failed to send message");
|
||||
return;
|
||||
}
|
||||
if (error instanceof ApiError) {
|
||||
setSubmitError(error.message || "Failed to send message");
|
||||
return;
|
||||
}
|
||||
setSubmitError(error instanceof Error ? error.message : "Failed to send message");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const form = useZodForm<PublicContactRequest>({
|
||||
schema: publicContactRequestSchema,
|
||||
initialValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
},
|
||||
onSubmit: handleSubmit,
|
||||
});
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className="max-w-lg mx-auto text-center py-16">
|
||||
<div className="w-20 h-20 bg-success/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<CheckCircle className="h-10 w-10 text-success" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-foreground mb-3">Message Sent!</h1>
|
||||
<p className="text-muted-foreground mb-8 text-lg">
|
||||
Thank you for contacting us. We'll get back to you within 24 hours.
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<Button as="a" href="/" variant="outline">
|
||||
Back to Home
|
||||
</Button>
|
||||
<Button as="a" href="/services">
|
||||
Browse Services
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 pb-0">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12 pt-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary/10 rounded-2xl mb-4 text-primary">
|
||||
<HelpCircle className="h-8 w-8" />
|
||||
<Send className="h-8 w-8" />
|
||||
</div>
|
||||
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-4 tracking-tight">
|
||||
We Speak Your Language
|
||||
Get in Touch
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
||||
Have a question about our services? Our English-speaking team is here to help. No Japanese
|
||||
required. Reach out through any channel below.
|
||||
Our English-speaking team is here to help. Fill out the form below or reach us through any
|
||||
of the channels listed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Contact Options */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-12">
|
||||
{/* Phone */}
|
||||
<a
|
||||
href="tel:0120-660-470"
|
||||
className="group bg-white rounded-2xl border border-border/60 p-6 hover:border-primary/40 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-colors">
|
||||
<Phone className="h-6 w-6" />
|
||||
{/* Two-column layout: Form + Sidebar */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10 mb-16">
|
||||
{/* Contact Form - takes 2/3 width */}
|
||||
<div className="lg:col-span-2">
|
||||
<ContactForm />
|
||||
</div>
|
||||
|
||||
{/* Sidebar - takes 1/3 width */}
|
||||
<div className="space-y-6">
|
||||
{/* 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"
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-foreground group-hover:text-primary transition-colors">
|
||||
<h3 className="font-bold text-foreground text-sm group-hover:text-primary transition-colors">
|
||||
Call Us
|
||||
</h3>
|
||||
<p className="text-lg font-bold text-primary">0120-660-470</p>
|
||||
<p className="text-base font-bold text-primary">0120-660-470</p>
|
||||
<p className="text-xs text-muted-foreground">Toll-free in Japan</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
{/* Chat */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
/* Trigger chat */
|
||||
}}
|
||||
className="group bg-white rounded-2xl border border-border/60 p-6 hover:border-blue-500/40 hover:shadow-md transition-all duration-200 text-left"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-12 w-12 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">
|
||||
<MessageSquare className="h-6 w-6" />
|
||||
{/* 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">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-foreground group-hover:text-blue-500 transition-colors">
|
||||
<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">
|
||||
@ -175,195 +72,60 @@ export function PublicContactView() {
|
||||
<span className="text-sm text-muted-foreground">Available now</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
|
||||
{/* Email */}
|
||||
<a
|
||||
href="mailto:support@assist-solutions.jp"
|
||||
className="group bg-white rounded-2xl border border-border/60 p-6 hover:border-emerald-500/40 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-12 w-12 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">
|
||||
<Mail className="h-6 w-6" />
|
||||
{/* 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"
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-foreground group-hover:text-emerald-500 transition-colors">
|
||||
<h3 className="font-bold text-foreground text-sm group-hover:text-emerald-500 transition-colors">
|
||||
Email Us
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">support@assist-solutions.jp</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Business Hours */}
|
||||
<div className="bg-muted/30 rounded-2xl p-5">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Clock className="h-5 w-5 text-muted-foreground" />
|
||||
<h3 className="font-bold text-foreground text-sm">Business Hours</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Mon - Fri, 9:30 AM - 6:00 PM JST</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Business Hours Banner */}
|
||||
<div className="bg-muted/30 rounded-xl p-4 mb-12 flex items-center justify-center gap-3 text-sm">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
<span className="font-medium text-foreground">Business Hours:</span> Mon - Fri, 9:30 AM -
|
||||
6:00 PM JST
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 mb-16">
|
||||
{/* FAQ Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-2">
|
||||
<HelpCircle className="h-6 w-6 text-primary" />
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{FAQ_ITEMS.map((item, index) => {
|
||||
const isExpanded = expandedFaq === index;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-xl border border-border/60 overflow-hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedFaq(isExpanded ? null : index)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="font-medium text-foreground text-sm pr-4">
|
||||
{item.question}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 text-muted-foreground flex-shrink-0 transition-transform",
|
||||
isExpanded && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{item.answer}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-2">
|
||||
<Send className="h-6 w-6 text-primary" />
|
||||
Send a Message
|
||||
</h2>
|
||||
<div className="bg-white rounded-2xl border border-border/60 p-6">
|
||||
{submitError && (
|
||||
<AlertBanner variant="error" title="Error" className="mb-6">
|
||||
{submitError}
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
<form onSubmit={event => void form.handleSubmit(event)} className="space-y-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<FormField
|
||||
label="Name"
|
||||
error={form.touched["name"] ? form.errors["name"] : undefined}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
value={form.values.name}
|
||||
onChange={e => form.setValue("name", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("name")}
|
||||
placeholder="Your name"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Email"
|
||||
error={form.touched["email"] ? form.errors["email"] : undefined}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="email"
|
||||
value={form.values.email}
|
||||
onChange={e => form.setValue("email", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("email")}
|
||||
placeholder="your@email.com"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<FormField
|
||||
label="Phone"
|
||||
error={form.touched["phone"] ? form.errors["phone"] : undefined}
|
||||
>
|
||||
<Input
|
||||
value={form.values.phone ?? ""}
|
||||
onChange={e => form.setValue("phone", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("phone")}
|
||||
placeholder="+81 90-1234-5678"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Subject"
|
||||
error={form.touched["subject"] ? form.errors["subject"] : undefined}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
value={form.values.subject}
|
||||
onChange={e => form.setValue("subject", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("subject")}
|
||||
placeholder="How can we help?"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
label="Message"
|
||||
error={form.touched["message"] ? form.errors["message"] : undefined}
|
||||
required
|
||||
>
|
||||
<textarea
|
||||
className="flex min-h-[120px] w-full rounded-lg border border-input bg-muted/20 px-4 py-3 text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:border-transparent disabled:cursor-not-allowed disabled:opacity-50 transition-colors resize-y text-sm"
|
||||
value={form.values.message}
|
||||
onChange={e => form.setValue("message", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("message")}
|
||||
placeholder="Tell us more about your inquiry..."
|
||||
rows={4}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={form.isSubmitting}
|
||||
isLoading={form.isSubmitting}
|
||||
loadingText="Sending..."
|
||||
>
|
||||
Send Message
|
||||
</Button>
|
||||
</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.
|
||||
{/* Office Location */}
|
||||
<div className="bg-white 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>
|
||||
</div>
|
||||
<address className="text-sm text-muted-foreground leading-relaxed not-italic mb-3">
|
||||
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu,
|
||||
<br />
|
||||
Minato-ku, Tokyo 106-0044
|
||||
</address>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
5 min walk from Exit 6, Azabu-Juban Station
|
||||
</p>
|
||||
<a
|
||||
href="https://www.google.com/maps/dir//Assist+Solutions+Corp,+3-8-2+Higashi+Azabu,+Minato-ku,+Tokyo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-primary text-sm font-medium hover:underline"
|
||||
>
|
||||
Get Directions
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Office Location */}
|
||||
<div className="bg-white rounded-2xl border border-border/60 overflow-hidden">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2">
|
||||
{/* Map */}
|
||||
<div className="h-[300px] lg:h-auto">
|
||||
{/* Small Map */}
|
||||
<div className="rounded-2xl overflow-hidden border border-border/60 h-[200px]">
|
||||
<iframe
|
||||
title="Assist Solutions Corp Office"
|
||||
src="https://www.google.com/maps?q=Assist+Solutions+Corp,+3-8-2+Higashi+Azabu,+Minato-ku,+Tokyo&output=embed"
|
||||
@ -373,82 +135,8 @@ export function PublicContactView() {
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Address Info */}
|
||||
<div className="p-8">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
|
||||
<MapPin className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground">Visit Our Office</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Walk-ins welcome during business hours
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||
Address
|
||||
</h3>
|
||||
<address className="text-foreground leading-relaxed not-italic">
|
||||
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu,
|
||||
<br />
|
||||
Minato-ku, Tokyo 106-0044
|
||||
</address>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||
Contact
|
||||
</h3>
|
||||
<p className="text-foreground">
|
||||
Tel: 03-3560-1006
|
||||
<br />
|
||||
Fax: 03-3560-1007
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||
Access
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
5 min walk from Exit 6 of Azabu-Juban Station
|
||||
<br />
|
||||
(Subway Oedo Line / Nanboku Line)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://www.google.com/maps/dir//Assist+Solutions+Corp,+3-8-2+Higashi+Azabu,+Minato-ku,+Tokyo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-primary font-medium hover:underline"
|
||||
>
|
||||
Get Directions
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Existing Customer CTA */}
|
||||
<div className="text-center mt-12 pt-8 border-t border-border/60">
|
||||
<p className="text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="font-semibold text-primary hover:text-primary/80 hover:underline transition-colors"
|
||||
>
|
||||
Sign in
|
||||
</Link>{" "}
|
||||
to access your dashboard and support tickets.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,116 +1,270 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { HelpCircle, MessageSquare, Mail, ChevronRight } from "lucide-react";
|
||||
import {
|
||||
HelpCircle,
|
||||
Wifi,
|
||||
Smartphone,
|
||||
Lock,
|
||||
Building2,
|
||||
CreditCard,
|
||||
Wrench,
|
||||
Download,
|
||||
ChevronDown,
|
||||
Send,
|
||||
} from "lucide-react";
|
||||
import { ContactForm } from "@/features/support/components";
|
||||
import { supportDownloads } from "@/features/landing-page/data";
|
||||
import { cn } from "@/shared/utils";
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
// =============================================================================
|
||||
// DATA
|
||||
// =============================================================================
|
||||
|
||||
const KNOWLEDGE_BASE_CATEGORIES = [
|
||||
{
|
||||
question: "Sample Question 1?",
|
||||
answer:
|
||||
"This is a sample answer for frequently asked question 1. Replace this with actual content when available.",
|
||||
title: "Internet & Wi-Fi",
|
||||
description: "Router setup, connection issues, speed troubleshooting",
|
||||
icon: Wifi,
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-500/10",
|
||||
hoverBorder: "hover:border-blue-500/40",
|
||||
},
|
||||
{
|
||||
question: "Sample Question 2?",
|
||||
answer:
|
||||
"This is a sample answer for frequently asked question 2. Replace this with actual content when available.",
|
||||
title: "Phone & SIM",
|
||||
description: "SIM activation, plan changes, number porting",
|
||||
icon: Smartphone,
|
||||
color: "text-green-500",
|
||||
bgColor: "bg-green-500/10",
|
||||
hoverBorder: "hover:border-green-500/40",
|
||||
},
|
||||
{
|
||||
question: "Sample Question 3?",
|
||||
answer:
|
||||
"This is a sample answer for frequently asked question 3. Replace this with actual content when available.",
|
||||
title: "VPN & Streaming",
|
||||
description: "VPN router setup, streaming access, configuration",
|
||||
icon: Lock,
|
||||
color: "text-purple-500",
|
||||
bgColor: "bg-purple-500/10",
|
||||
hoverBorder: "hover:border-purple-500/40",
|
||||
},
|
||||
{
|
||||
question: "Sample Question 4?",
|
||||
answer:
|
||||
"This is a sample answer for frequently asked question 4. Replace this with actual content when available.",
|
||||
title: "Business Solutions",
|
||||
description: "Office networks, dedicated lines, enterprise support",
|
||||
icon: Building2,
|
||||
color: "text-orange-500",
|
||||
bgColor: "bg-orange-500/10",
|
||||
hoverBorder: "hover:border-orange-500/40",
|
||||
},
|
||||
{
|
||||
title: "Billing & Account",
|
||||
description: "Invoices, payments, account changes, contracts",
|
||||
icon: CreditCard,
|
||||
color: "text-pink-500",
|
||||
bgColor: "bg-pink-500/10",
|
||||
hoverBorder: "hover:border-pink-500/40",
|
||||
},
|
||||
{
|
||||
title: "General Tech Support",
|
||||
description: "Device help, software issues, general troubleshooting",
|
||||
icon: Wrench,
|
||||
color: "text-amber-500",
|
||||
bgColor: "bg-amber-500/10",
|
||||
hoverBorder: "hover:border-amber-500/40",
|
||||
},
|
||||
];
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{
|
||||
question: "How do I set up my internet router?",
|
||||
answer:
|
||||
"After your installation appointment, connect the provided router to the NTT ONU device using the included LAN cable. Power on the router and connect to the Wi-Fi network using the credentials on the router label. If you need help, contact our support team or use our remote support tools.",
|
||||
},
|
||||
{
|
||||
question: "How do I activate my SIM card?",
|
||||
answer:
|
||||
"Insert the SIM card into your unlocked phone. You should receive a confirmation email with APN settings. Go to Settings > Mobile Data > APN and enter the provided settings. Restart your phone and you should be connected within a few minutes.",
|
||||
},
|
||||
{
|
||||
question: "What payment methods do you accept?",
|
||||
answer:
|
||||
"We accept major credit cards (Visa, Mastercard, American Express), bank transfers, and convenience store payments. Foreign credit cards are accepted for all our services.",
|
||||
},
|
||||
{
|
||||
question: "How do I contact support outside business hours?",
|
||||
answer:
|
||||
"You can send us an email or submit a contact form at any time — we'll respond within 24 hours on the next business day. For urgent issues, our live chat may have extended availability.",
|
||||
},
|
||||
{
|
||||
question: "Can I change my plan after signing up?",
|
||||
answer:
|
||||
"Yes, you can change your plan at any time. Contact our support team and we'll help you switch to a plan that better fits your needs. Changes typically take effect from the next billing cycle.",
|
||||
},
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* PublicSupportView - Public FAQ and support landing page
|
||||
* PublicSupportView - Self-service support hub
|
||||
*/
|
||||
export function PublicSupportView() {
|
||||
const [expandedFaq, setExpandedFaq] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-16">
|
||||
<div className="max-w-6xl mx-auto px-4 pb-0">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary/10 rounded-2xl mb-2 text-primary">
|
||||
<div className="text-center mb-12 pt-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary/10 rounded-2xl mb-4 text-primary">
|
||||
<HelpCircle className="h-8 w-8" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-foreground">How Can We Help?</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Questions about our services? Our English-speaking team is ready to assist. Find answers
|
||||
below or reach out directly.
|
||||
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-4 tracking-tight">
|
||||
How Can We Help?
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
||||
Find answers, download remote support tools, or send us a message. Our English-speaking
|
||||
team is ready to assist.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Contact Options */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="group bg-card rounded-2xl border border-border p-6 hover:border-primary/50 hover:shadow-md transition-all duration-300"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center flex-shrink-0 text-primary">
|
||||
<Mail className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||
Send us a message
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
|
||||
Fill out our contact form and we'll get back to you within 24 hours.
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors mt-1" />
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<a
|
||||
href="mailto:support@assist-solutions.jp"
|
||||
className="group bg-card rounded-2xl border border-border p-6 hover:border-primary/50 hover:shadow-md transition-all duration-300"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 bg-blue-500/10 rounded-xl flex items-center justify-center flex-shrink-0 text-blue-500">
|
||||
<MessageSquare className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||
Email Support
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed truncate">
|
||||
support@assist-solutions.jp
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors mt-1" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-semibold text-foreground text-center">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<div className="bg-card rounded-2xl border border-border divide-y divide-border">
|
||||
{FAQ_ITEMS.map((item, index) => (
|
||||
<details key={index} className="group">
|
||||
<summary className="flex items-center justify-between cursor-pointer list-none p-6 hover:bg-muted/30 transition-colors">
|
||||
<span className="font-medium text-foreground pr-4 group-hover:text-primary transition-colors">
|
||||
{item.question}
|
||||
</span>
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground group-open:rotate-90 transition-transform flex-shrink-0" />
|
||||
</summary>
|
||||
<div className="px-6 pb-6 pt-0">
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">{item.answer}</p>
|
||||
{/* Knowledge Base Categories */}
|
||||
<section className="mb-16">
|
||||
<h2 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 className="flex items-start gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
"h-11 w-11 rounded-xl flex items-center justify-center shrink-0",
|
||||
category.bgColor,
|
||||
category.color
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-foreground text-sm mb-1">{category.title}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{category.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Remote Support Tools */}
|
||||
<section className="mb-16">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-2 text-center">
|
||||
Remote Support Tools
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-center mb-6">
|
||||
Download one of these tools so our technicians can assist you remotely.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{supportDownloads.map(tool => (
|
||||
<a
|
||||
key={tool.title}
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Existing Customer */}
|
||||
<div className="text-center pt-8 border-t border-border">
|
||||
{/* FAQ */}
|
||||
<section className="mb-16">
|
||||
<h2 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">
|
||||
{FAQ_ITEMS.map((item, index) => {
|
||||
const isExpanded = expandedFaq === index;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-xl border border-border/60 overflow-hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedFaq(isExpanded ? null : index)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="font-medium text-foreground text-sm pr-4">{item.question}</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 text-muted-foreground flex-shrink-0 transition-transform",
|
||||
isExpanded && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{item.answer}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Form Fallback */}
|
||||
<section className="mb-12">
|
||||
<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>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Send us a message and we'll get back to you within 24 hours.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<ContactForm />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Existing Customer CTA */}
|
||||
<div className="text-center pt-8 border-t border-border/60">
|
||||
<p className="text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
@ -119,7 +273,7 @@ export function PublicSupportView() {
|
||||
>
|
||||
Sign in
|
||||
</Link>{" "}
|
||||
to access your dashboard and support cases.
|
||||
to access your dashboard and support tickets.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,3 +2,5 @@ export * from "./NewSupportCaseView";
|
||||
export * from "./SupportCasesView";
|
||||
export * from "./SupportCaseDetailView";
|
||||
export * from "./SupportHomeView";
|
||||
export * from "./PublicContactView";
|
||||
export * from "./PublicSupportView";
|
||||
|
||||
@ -162,6 +162,17 @@
|
||||
letter-spacing: var(--cp-tracking-tight);
|
||||
}
|
||||
|
||||
/* Semantic heading presets — composite utilities for common heading patterns.
|
||||
Use these instead of manually combining text-display-* + font-* + leading-*. */
|
||||
|
||||
@utility text-section-heading {
|
||||
font-size: var(--cp-text-display-sm);
|
||||
font-family: var(--font-display);
|
||||
font-weight: var(--cp-font-semibold);
|
||||
line-height: var(--cp-leading-tight);
|
||||
letter-spacing: var(--cp-tracking-tight);
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* ===== PAGE ENTRANCE ANIMATIONS ===== */
|
||||
.cp-animate-in {
|
||||
|
||||
114
docs/plans/2026-03-04-public-pages-restructuring-design.md
Normal file
114
docs/plans/2026-03-04-public-pages-restructuring-design.md
Normal file
@ -0,0 +1,114 @@
|
||||
# Public Pages Restructuring Design
|
||||
|
||||
**Date**: 2026-03-04
|
||||
**Scope**: Homepage hero update, Contact page cleanup, Support page creation, shared ContactForm component
|
||||
|
||||
## Problem
|
||||
|
||||
- Homepage hero text doesn't match desired branding ("A One Stop Solution for Your IT Needs")
|
||||
- Contact page mixes FAQ and contact form — unfocused
|
||||
- Support page is thin with placeholder content
|
||||
- Acronis/TeamViewer remote support tools are defined in data but not rendered anywhere
|
||||
- Contact form is hardcoded in PublicContactView — not reusable
|
||||
- `/help` just redirects to `/contact`, no dedicated support hub
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Homepage (`/`)
|
||||
|
||||
**Hero Section changes only** — rest of page (TrustStrip, ServicesGrid, WhyUsSection, CTABanner) stays as-is.
|
||||
|
||||
- **Heading line 1**: "A One Stop Solution"
|
||||
- **Heading line 2** (primary color): "for Your IT Needs"
|
||||
- **Subtitle**: "From internet and mobile to VPN and on-site tech support — we handle it all in English so you don't have to."
|
||||
- **CTAs**: "Browse Services" (primary) + "Need Assistance?" (outline, links to `/contact`)
|
||||
|
||||
### 2. Contact Page (`/contact`)
|
||||
|
||||
Focused page for reaching a human. Two-column layout.
|
||||
|
||||
**Left column — Contact Form**:
|
||||
|
||||
- Reusable `ContactForm` component
|
||||
- Fields: Name, Email, Phone (optional), Subject, Message
|
||||
- Same Zod validation (`publicContactRequestSchema`)
|
||||
- Submit → POST `/api/support/contact` → success state
|
||||
|
||||
**Right sidebar — Contact Info** (stacked cards):
|
||||
|
||||
- Quick contact options: Phone (0120-660-470), Email (support@assist-solutions.jp), Live Chat
|
||||
- Business hours: Mon-Fri 9:30 AM - 6:00 PM JST
|
||||
- Office location: address, access info, "Get Directions" link
|
||||
- Small embedded Google Map
|
||||
|
||||
**Removed from contact page**: FAQ section (moves to Support)
|
||||
|
||||
### 3. Support Page (`/support`)
|
||||
|
||||
Self-service support hub. Top-to-bottom flow.
|
||||
|
||||
**Section 1: Header**
|
||||
|
||||
- Icon + "How Can We Help?" heading
|
||||
- Subtitle about self-service and English support
|
||||
|
||||
**Section 2: Knowledge Base Categories**
|
||||
|
||||
- Grid of 6 category cards (2x3 on desktop, 1 col mobile):
|
||||
- Internet & Wi-Fi
|
||||
- Phone & SIM
|
||||
- VPN & Streaming
|
||||
- Business Solutions
|
||||
- Billing & Account
|
||||
- General Tech Support
|
||||
- Each card has icon, title, brief description
|
||||
- Cards can link to anchors or future article pages (placeholder for now)
|
||||
|
||||
**Section 3: Remote Support Tools**
|
||||
|
||||
- Uses existing `supportDownloads` data from `features/landing-page/data/services.tsx`
|
||||
- Two cards side-by-side: Acronis Quick Assist + TeamViewer QS
|
||||
- Each shows: logo image, title, description, use case, download link (external)
|
||||
|
||||
**Section 4: FAQ**
|
||||
|
||||
- Accordion-style FAQ items (moved from contact page)
|
||||
- Placeholder content for now, real content to be added later
|
||||
|
||||
**Section 5: Contact Form Fallback**
|
||||
|
||||
- "Still need help? Send us a message" heading
|
||||
- Reusable `ContactForm` component
|
||||
|
||||
### 4. Shared ContactForm Component
|
||||
|
||||
**Location**: `apps/portal/src/features/support/components/ContactForm.tsx`
|
||||
|
||||
Extracted from current `PublicContactView`:
|
||||
|
||||
- Same fields, same Zod schema, same API call
|
||||
- Accepts optional `className` prop
|
||||
- Self-contained: manages own submit state and success/error UI
|
||||
- Exported from `features/support/components/index.ts`
|
||||
|
||||
## File Changes
|
||||
|
||||
### New files
|
||||
|
||||
- `features/support/components/ContactForm.tsx` — reusable form
|
||||
- `app/(public)/(site)/support/page.tsx` — support page route
|
||||
|
||||
### Modified files
|
||||
|
||||
- `features/landing-page/components/HeroSection.tsx` — new heading + subtitle
|
||||
- `features/support/views/PublicContactView.tsx` — two-column layout, remove FAQ, use ContactForm
|
||||
- `features/support/views/PublicSupportView.tsx` — full rewrite as support hub
|
||||
- `features/support/views/index.ts` — update exports if needed
|
||||
- `features/support/components/index.ts` — export ContactForm
|
||||
|
||||
### Routing
|
||||
|
||||
- `/` — homepage (updated hero)
|
||||
- `/contact` — contact page (cleaned up)
|
||||
- `/support` — new support hub page
|
||||
- `/help` — keep redirect to `/contact` (backwards compat)
|
||||
883
docs/plans/2026-03-04-public-pages-restructuring-plan.md
Normal file
883
docs/plans/2026-03-04-public-pages-restructuring-plan.md
Normal file
@ -0,0 +1,883 @@
|
||||
# Public Pages Restructuring Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Restructure public pages — update homepage hero, extract reusable ContactForm, clean up Contact page (form + sidebar), create Support page (knowledge base categories, remote tools, FAQ, contact form fallback).
|
||||
|
||||
**Architecture:** Extract shared ContactForm from PublicContactView into `features/support/components/`. Rewrite PublicContactView as focused two-column contact page. Rewrite PublicSupportView as self-service hub. Update HeroSection text. Add `/support` route.
|
||||
|
||||
**Tech Stack:** Next.js 15, React 19, Tailwind CSS, shadcn/ui atoms, Zod validation, lucide-react icons
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Update Homepage Hero Text
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/features/landing-page/components/HeroSection.tsx:42-49`
|
||||
|
||||
**Step 1: Update the hero heading and subtitle**
|
||||
|
||||
Change lines 42-49 in `HeroSection.tsx`:
|
||||
|
||||
```tsx
|
||||
// OLD:
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold leading-tight text-foreground">
|
||||
<span className="block">English IT Support</span>
|
||||
<span className="block text-primary mt-2">for Expats in Japan</span>
|
||||
</h1>
|
||||
<p className="mt-6 text-base sm:text-lg text-muted-foreground leading-relaxed font-semibold max-w-2xl mx-auto">
|
||||
No Japanese required. Get reliable internet, mobile, and VPN services with full English
|
||||
support. Serving expats and international businesses for over 20 years.
|
||||
</p>
|
||||
|
||||
// NEW:
|
||||
<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>
|
||||
<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>
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
feat: update homepage hero text to "A One Stop Solution for Your IT Needs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Extract Reusable ContactForm Component
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/portal/src/features/support/components/ContactForm.tsx`
|
||||
- Create: `apps/portal/src/features/support/components/index.ts`
|
||||
|
||||
**Step 1: Create the ContactForm component**
|
||||
|
||||
Create `apps/portal/src/features/support/components/ContactForm.tsx`:
|
||||
|
||||
```tsx
|
||||
"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";
|
||||
import { useZodForm } from "@/shared/hooks";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
import {
|
||||
publicContactRequestSchema,
|
||||
type PublicContactRequest,
|
||||
} from "@customer-portal/domain/support";
|
||||
import { apiClient, ApiError, isApiError } from "@/core/api";
|
||||
import { cn } from "@/shared/utils";
|
||||
|
||||
interface ContactFormProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ContactForm({ className }: ContactFormProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = useCallback(async (data: PublicContactRequest) => {
|
||||
setSubmitError(null);
|
||||
|
||||
try {
|
||||
await apiClient.POST("/api/support/contact", { body: data });
|
||||
setIsSubmitted(true);
|
||||
} catch (error) {
|
||||
if (isApiError(error)) {
|
||||
setSubmitError(error.message || "Failed to send message");
|
||||
return;
|
||||
}
|
||||
if (error instanceof ApiError) {
|
||||
setSubmitError(error.message || "Failed to send message");
|
||||
return;
|
||||
}
|
||||
setSubmitError(error instanceof Error ? error.message : "Failed to send message");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const form = useZodForm<PublicContactRequest>({
|
||||
schema: publicContactRequestSchema,
|
||||
initialValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
},
|
||||
onSubmit: handleSubmit,
|
||||
});
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className={cn("text-center py-12", className)}>
|
||||
<div className="w-16 h-16 bg-success/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="h-8 w-8 text-success" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-foreground mb-2">Message Sent!</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Thank you for contacting us. We'll get back to you within 24 hours.
|
||||
</p>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Button as="a" href="/" variant="outline" size="sm">
|
||||
Back to Home
|
||||
</Button>
|
||||
<Button as="a" href="/services" size="sm">
|
||||
Browse Services
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("bg-white rounded-2xl border border-border/60 p-6", className)}>
|
||||
{submitError && (
|
||||
<AlertBanner variant="error" title="Error" className="mb-6">
|
||||
{submitError}
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
<form onSubmit={event => void form.handleSubmit(event)} className="space-y-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<FormField
|
||||
label="Name"
|
||||
error={form.touched["name"] ? form.errors["name"] : undefined}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
value={form.values.name}
|
||||
onChange={e => form.setValue("name", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("name")}
|
||||
placeholder="Your name"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Email"
|
||||
error={form.touched["email"] ? form.errors["email"] : undefined}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="email"
|
||||
value={form.values.email}
|
||||
onChange={e => form.setValue("email", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("email")}
|
||||
placeholder="your@email.com"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<FormField label="Phone" error={form.touched["phone"] ? form.errors["phone"] : undefined}>
|
||||
<Input
|
||||
value={form.values.phone ?? ""}
|
||||
onChange={e => form.setValue("phone", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("phone")}
|
||||
placeholder="+81 90-1234-5678"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Subject"
|
||||
error={form.touched["subject"] ? form.errors["subject"] : undefined}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
value={form.values.subject}
|
||||
onChange={e => form.setValue("subject", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("subject")}
|
||||
placeholder="How can we help?"
|
||||
className="bg-muted/20"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
label="Message"
|
||||
error={form.touched["message"] ? form.errors["message"] : undefined}
|
||||
required
|
||||
>
|
||||
<textarea
|
||||
className="flex min-h-[120px] w-full rounded-lg border border-input bg-muted/20 px-4 py-3 text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:border-transparent disabled:cursor-not-allowed disabled:opacity-50 transition-colors resize-y text-sm"
|
||||
value={form.values.message}
|
||||
onChange={e => form.setValue("message", e.target.value)}
|
||||
onBlur={() => form.setTouchedField("message")}
|
||||
placeholder="Tell us more about your inquiry..."
|
||||
rows={4}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={form.isSubmitting}
|
||||
isLoading={form.isSubmitting}
|
||||
loadingText="Sending..."
|
||||
>
|
||||
Send Message
|
||||
</Button>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Create barrel export**
|
||||
|
||||
Create `apps/portal/src/features/support/components/index.ts`:
|
||||
|
||||
```ts
|
||||
export { ContactForm } from "./ContactForm";
|
||||
```
|
||||
|
||||
**Step 3: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal`
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```
|
||||
feat: extract reusable ContactForm component from PublicContactView
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Rewrite Contact Page (Two-Column Layout)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/features/support/views/PublicContactView.tsx` (full rewrite)
|
||||
|
||||
**Step 1: Rewrite PublicContactView**
|
||||
|
||||
Replace the entire content of `apps/portal/src/features/support/views/PublicContactView.tsx` with:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { Mail, MapPin, Phone, MessageSquare, Clock, Send, ExternalLink } from "lucide-react";
|
||||
import { ContactForm } from "@/features/support/components";
|
||||
|
||||
/**
|
||||
* PublicContactView - Focused contact page with form + sidebar info
|
||||
*/
|
||||
export function PublicContactView() {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 pb-0">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12 pt-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary/10 rounded-2xl mb-4 text-primary">
|
||||
<Send className="h-8 w-8" />
|
||||
</div>
|
||||
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-4 tracking-tight">
|
||||
Get in Touch
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
||||
Our English-speaking team is here to help. Fill out the form below or reach us through any
|
||||
of the channels listed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Two-column layout: Form + Sidebar */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10 mb-16">
|
||||
{/* Contact Form - takes 2/3 width */}
|
||||
<div className="lg:col-span-2">
|
||||
<ContactForm />
|
||||
</div>
|
||||
|
||||
{/* Sidebar - takes 1/3 width */}
|
||||
<div className="space-y-6">
|
||||
{/* 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"
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-foreground text-sm group-hover:text-primary transition-colors">
|
||||
Call Us
|
||||
</h3>
|
||||
<p className="text-base font-bold text-primary">0120-660-470</p>
|
||||
<p className="text-xs text-muted-foreground">Toll-free in Japan</p>
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Email */}
|
||||
<a
|
||||
href="mailto:support@assist-solutions.jp"
|
||||
className="group flex items-center gap-4 bg-white rounded-2xl border border-border/60 p-5 hover:border-emerald-500/40 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-foreground text-sm group-hover:text-emerald-500 transition-colors">
|
||||
Email Us
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">support@assist-solutions.jp</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Business Hours */}
|
||||
<div className="bg-muted/30 rounded-2xl p-5">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Clock className="h-5 w-5 text-muted-foreground" />
|
||||
<h3 className="font-bold text-foreground text-sm">Business Hours</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Mon - Fri, 9:30 AM - 6:00 PM JST</p>
|
||||
</div>
|
||||
|
||||
{/* Office Location */}
|
||||
<div className="bg-white 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>
|
||||
</div>
|
||||
<address className="text-sm text-muted-foreground leading-relaxed not-italic mb-3">
|
||||
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu,
|
||||
<br />
|
||||
Minato-ku, Tokyo 106-0044
|
||||
</address>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
5 min walk from Exit 6, Azabu-Juban Station
|
||||
</p>
|
||||
<a
|
||||
href="https://www.google.com/maps/dir//Assist+Solutions+Corp,+3-8-2+Higashi+Azabu,+Minato-ku,+Tokyo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-primary text-sm font-medium hover:underline"
|
||||
>
|
||||
Get Directions
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Small Map */}
|
||||
<div className="rounded-2xl overflow-hidden border border-border/60 h-[200px]">
|
||||
<iframe
|
||||
title="Assist Solutions Corp Office"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicContactView;
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
feat: rewrite contact page with two-column layout using shared ContactForm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Rewrite Support Page as Self-Service Hub
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/features/support/views/PublicSupportView.tsx` (full rewrite)
|
||||
|
||||
**Step 1: Rewrite PublicSupportView**
|
||||
|
||||
Replace the entire content of `apps/portal/src/features/support/views/PublicSupportView.tsx` with:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
HelpCircle,
|
||||
Wifi,
|
||||
Smartphone,
|
||||
Lock,
|
||||
Building2,
|
||||
CreditCard,
|
||||
Wrench,
|
||||
Download,
|
||||
ChevronDown,
|
||||
Send,
|
||||
} from "lucide-react";
|
||||
import { ContactForm } from "@/features/support/components";
|
||||
import { supportDownloads } from "@/features/landing-page/data";
|
||||
import { cn } from "@/shared/utils";
|
||||
|
||||
// =============================================================================
|
||||
// DATA
|
||||
// =============================================================================
|
||||
|
||||
const KNOWLEDGE_BASE_CATEGORIES = [
|
||||
{
|
||||
title: "Internet & Wi-Fi",
|
||||
description: "Router setup, connection issues, speed troubleshooting",
|
||||
icon: Wifi,
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-500/10",
|
||||
hoverBorder: "hover:border-blue-500/40",
|
||||
},
|
||||
{
|
||||
title: "Phone & SIM",
|
||||
description: "SIM activation, plan changes, number porting",
|
||||
icon: Smartphone,
|
||||
color: "text-green-500",
|
||||
bgColor: "bg-green-500/10",
|
||||
hoverBorder: "hover:border-green-500/40",
|
||||
},
|
||||
{
|
||||
title: "VPN & Streaming",
|
||||
description: "VPN router setup, streaming access, configuration",
|
||||
icon: Lock,
|
||||
color: "text-purple-500",
|
||||
bgColor: "bg-purple-500/10",
|
||||
hoverBorder: "hover:border-purple-500/40",
|
||||
},
|
||||
{
|
||||
title: "Business Solutions",
|
||||
description: "Office networks, dedicated lines, enterprise support",
|
||||
icon: Building2,
|
||||
color: "text-orange-500",
|
||||
bgColor: "bg-orange-500/10",
|
||||
hoverBorder: "hover:border-orange-500/40",
|
||||
},
|
||||
{
|
||||
title: "Billing & Account",
|
||||
description: "Invoices, payments, account changes, contracts",
|
||||
icon: CreditCard,
|
||||
color: "text-pink-500",
|
||||
bgColor: "bg-pink-500/10",
|
||||
hoverBorder: "hover:border-pink-500/40",
|
||||
},
|
||||
{
|
||||
title: "General Tech Support",
|
||||
description: "Device help, software issues, general troubleshooting",
|
||||
icon: Wrench,
|
||||
color: "text-amber-500",
|
||||
bgColor: "bg-amber-500/10",
|
||||
hoverBorder: "hover:border-amber-500/40",
|
||||
},
|
||||
];
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{
|
||||
question: "How do I set up my internet router?",
|
||||
answer:
|
||||
"After your installation appointment, connect the provided router to the NTT ONU device using the included LAN cable. Power on the router and connect to the Wi-Fi network using the credentials on the router label. If you need help, contact our support team or use our remote support tools.",
|
||||
},
|
||||
{
|
||||
question: "How do I activate my SIM card?",
|
||||
answer:
|
||||
"Insert the SIM card into your unlocked phone. You should receive a confirmation email with APN settings. Go to Settings > Mobile Data > APN and enter the provided settings. Restart your phone and you should be connected within a few minutes.",
|
||||
},
|
||||
{
|
||||
question: "What payment methods do you accept?",
|
||||
answer:
|
||||
"We accept major credit cards (Visa, Mastercard, American Express), bank transfers, and convenience store payments. Foreign credit cards are accepted for all our services.",
|
||||
},
|
||||
{
|
||||
question: "How do I contact support outside business hours?",
|
||||
answer:
|
||||
"You can send us an email or submit a contact form at any time — we'll respond within 24 hours on the next business day. For urgent issues, our live chat may have extended availability.",
|
||||
},
|
||||
{
|
||||
question: "Can I change my plan after signing up?",
|
||||
answer:
|
||||
"Yes, you can change your plan at any time. Contact our support team and we'll help you switch to a plan that better fits your needs. Changes typically take effect from the next billing cycle.",
|
||||
},
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* PublicSupportView - Self-service support hub
|
||||
*/
|
||||
export function PublicSupportView() {
|
||||
const [expandedFaq, setExpandedFaq] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 pb-0">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12 pt-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary/10 rounded-2xl mb-4 text-primary">
|
||||
<HelpCircle className="h-8 w-8" />
|
||||
</div>
|
||||
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-4 tracking-tight">
|
||||
How Can We Help?
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
||||
Find answers, download remote support tools, or send us a message. Our English-speaking
|
||||
team is ready to assist.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Knowledge Base Categories */}
|
||||
<section className="mb-16">
|
||||
<h2 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 className="flex items-start gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
"h-11 w-11 rounded-xl flex items-center justify-center shrink-0",
|
||||
category.bgColor,
|
||||
category.color
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-foreground text-sm mb-1">{category.title}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{category.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Remote Support Tools */}
|
||||
<section className="mb-16">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-2 text-center">
|
||||
Remote Support Tools
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-center mb-6">
|
||||
Download one of these tools so our technicians can assist you remotely.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{supportDownloads.map(tool => (
|
||||
<a
|
||||
key={tool.title}
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</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">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<div className="max-w-3xl mx-auto space-y-3">
|
||||
{FAQ_ITEMS.map((item, index) => {
|
||||
const isExpanded = expandedFaq === index;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-xl border border-border/60 overflow-hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedFaq(isExpanded ? null : index)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="font-medium text-foreground text-sm pr-4">{item.question}</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 text-muted-foreground flex-shrink-0 transition-transform",
|
||||
isExpanded && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{item.answer}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Form Fallback */}
|
||||
<section className="mb-12">
|
||||
<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>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Send us a message and we'll get back to you within 24 hours.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<ContactForm />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Existing Customer CTA */}
|
||||
<div className="text-center pt-8 border-t border-border/60">
|
||||
<p className="text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="font-semibold text-primary hover:text-primary/80 hover:underline transition-colors"
|
||||
>
|
||||
Sign in
|
||||
</Link>{" "}
|
||||
to access your dashboard and support tickets.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicSupportView;
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal`
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```
|
||||
feat: rewrite support page as self-service hub with knowledge base, remote tools, FAQ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add Support Page Route
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/portal/src/app/(public)/(site)/support/page.tsx`
|
||||
|
||||
**Step 1: Create the support route page**
|
||||
|
||||
Create `apps/portal/src/app/(public)/(site)/support/page.tsx`:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* Public Support Page
|
||||
*
|
||||
* Self-service support hub with knowledge base categories,
|
||||
* remote support tools, FAQ, and contact form fallback.
|
||||
*/
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { PublicSupportView } from "@/features/support/views/PublicSupportView";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Support - Self-Service Help Center | Assist Solutions",
|
||||
description:
|
||||
"Find answers to common questions, download remote support tools, or contact our English-speaking team. No Japanese required.",
|
||||
keywords: [
|
||||
"IT support Japan",
|
||||
"English tech support Tokyo",
|
||||
"remote support Japan",
|
||||
"expat tech help",
|
||||
"Assist Solutions support",
|
||||
],
|
||||
openGraph: {
|
||||
title: "Support - Help Center | Assist Solutions",
|
||||
description:
|
||||
"Self-service support hub for expats in Japan. FAQ, remote support tools, and direct contact options.",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function SupportPage() {
|
||||
return <PublicSupportView />;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal`
|
||||
|
||||
**Step 3: Verify type checking passes**
|
||||
|
||||
Run: `pnpm type-check`
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```
|
||||
feat: add /support route for public support page
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Update Support Views Barrel Export
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/features/support/views/index.ts`
|
||||
|
||||
**Step 1: Add PublicSupportView and PublicContactView to the barrel export**
|
||||
|
||||
The current `views/index.ts` only exports authenticated views. Add the public views:
|
||||
|
||||
```ts
|
||||
// Current content:
|
||||
export * from "./NewSupportCaseView";
|
||||
export * from "./SupportCasesView";
|
||||
export * from "./SupportCaseDetailView";
|
||||
export * from "./SupportHomeView";
|
||||
|
||||
// Add these:
|
||||
export * from "./PublicContactView";
|
||||
export * from "./PublicSupportView";
|
||||
```
|
||||
|
||||
Note: The contact page currently imports directly from the view file (`@/features/support/views/PublicContactView`), not from the barrel. After adding to barrel, update the contact page import in `apps/portal/src/app/(public)/(site)/contact/page.tsx` to use the barrel:
|
||||
|
||||
```tsx
|
||||
// OLD:
|
||||
import { PublicContactView } from "@/features/support/views/PublicContactView";
|
||||
|
||||
// NEW:
|
||||
import { PublicContactView } from "@/features/support/views";
|
||||
```
|
||||
|
||||
And update the new support page import in `apps/portal/src/app/(public)/(site)/support/page.tsx`:
|
||||
|
||||
```tsx
|
||||
// OLD:
|
||||
import { PublicSupportView } from "@/features/support/views/PublicSupportView";
|
||||
|
||||
// NEW:
|
||||
import { PublicSupportView } from "@/features/support/views";
|
||||
```
|
||||
|
||||
**Step 2: Verify no lint errors**
|
||||
|
||||
Run: `pnpm lint --filter @customer-portal/portal`
|
||||
|
||||
**Step 3: Verify type checking passes**
|
||||
|
||||
Run: `pnpm type-check`
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```
|
||||
refactor: add public views to support barrel exports
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Final Verification
|
||||
|
||||
**Step 1: Run full lint check**
|
||||
|
||||
Run: `pnpm lint`
|
||||
|
||||
**Step 2: Run type check**
|
||||
|
||||
Run: `pnpm type-check`
|
||||
|
||||
**Step 3: Verify all pages render** (manual or dev server if permitted)
|
||||
|
||||
Check these routes work:
|
||||
|
||||
- `/` — homepage with new hero text
|
||||
- `/contact` — two-column contact page
|
||||
- `/support` — self-service support hub
|
||||
- `/help` — still redirects to `/contact`
|
||||
Loading…
x
Reference in New Issue
Block a user