884 lines
30 KiB
Markdown
884 lines
30 KiB
Markdown
|
|
# 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`
|