Enhance Homepage UX with Mobile Navigation and Form Improvements
- Add mobile hamburger menu with full navigation - Fix touch support for Services dropdown - Add form validation and accessibility labels to contact form - Add carousel position indicators and swipe support - Add scroll-triggered animations with useInView hook - Add sticky mobile CTA bar - Improve Remote Support section mobile layout - Add animated background blobs - Add language selector (EN indicator) - Optimize hero image with priority flag Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ff870c9f4f
commit
c9d568d22f
118
CLAUDE.md
Normal file
118
CLAUDE.md
Normal file
@ -0,0 +1,118 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Customer Portal with BFF (Backend for Frontend) architecture. A pnpm monorepo with a Next.js 15 frontend, NestJS 11 backend, and shared domain layer.
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
pnpm dev:start # Start PostgreSQL + Redis services
|
||||
pnpm dev # Start both apps with hot reload
|
||||
pnpm dev:all # Build domain + start both apps
|
||||
|
||||
# Building
|
||||
pnpm build # Build all (domain first, then apps)
|
||||
pnpm domain:build # Build domain layer only
|
||||
|
||||
# Quality checks
|
||||
pnpm lint # Run ESLint across all packages
|
||||
pnpm type-check # TypeScript type checking
|
||||
pnpm format:check # Check Prettier formatting
|
||||
|
||||
# Database (Prisma)
|
||||
pnpm db:migrate # Run migrations
|
||||
pnpm db:generate # Generate Prisma client
|
||||
pnpm db:studio # Open Prisma Studio
|
||||
pnpm db:reset # Reset database
|
||||
|
||||
# Testing
|
||||
pnpm test # Run tests across packages
|
||||
pnpm --filter @customer-portal/bff test # BFF tests (node --test)
|
||||
|
||||
# Single package commands
|
||||
pnpm --filter @customer-portal/portal dev # Portal only
|
||||
pnpm --filter @customer-portal/bff dev # BFF only
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
|
||||
```
|
||||
apps/
|
||||
portal/ # Next.js 15 frontend (React 19, Tailwind CSS 4, shadcn/ui)
|
||||
bff/ # NestJS 11 backend (Prisma, PostgreSQL, Redis)
|
||||
packages/
|
||||
domain/ # Shared types, Zod schemas, mappers (framework-agnostic)
|
||||
```
|
||||
|
||||
### System Boundaries
|
||||
|
||||
- **Domain (`packages/domain/`)**: Shared contracts, Zod validation, cross-app utilities. Must be framework-agnostic.
|
||||
- **BFF (`apps/bff/`)**: NestJS HTTP boundary, orchestration, integrations (Salesforce/WHMCS/Freebit).
|
||||
- **Portal (`apps/portal/`)**: Next.js UI. Pages are thin wrappers over feature modules in `src/features/`.
|
||||
|
||||
### External Systems
|
||||
|
||||
- **WHMCS**: Billing, subscriptions, invoices, authoritative address storage
|
||||
- **Salesforce**: CRM (Accounts, Contacts, Cases), order management
|
||||
- **Freebit**: SIM management
|
||||
|
||||
### Data Flow
|
||||
|
||||
Portal → BFF → External Systems (WHMCS/Salesforce/Freebit)
|
||||
|
||||
User mapping: `user_id ↔ whmcs_client_id ↔ sf_contact_id/sf_account_id`
|
||||
|
||||
## Import Rules (ESLint Enforced)
|
||||
|
||||
```ts
|
||||
// ✅ Correct (Portal + BFF)
|
||||
import type { Invoice } from "@customer-portal/domain/billing";
|
||||
import { invoiceSchema } from "@customer-portal/domain/billing";
|
||||
|
||||
// ✅ Correct (BFF only)
|
||||
import { Whmcs } from "@customer-portal/domain/billing/providers";
|
||||
|
||||
// ❌ Forbidden
|
||||
import { Billing } from "@customer-portal/domain"; // no root imports
|
||||
import { Invoice } from "@customer-portal/domain/billing/contract"; // no deep imports
|
||||
```
|
||||
|
||||
Portal must never import `providers` from domain.
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Validation (Zod-first)
|
||||
|
||||
- Schemas live in `packages/domain/<module>/schema.ts`
|
||||
- Types are derived: `export type X = z.infer<typeof xSchema>`
|
||||
- Query params use `z.coerce.*` for URL strings
|
||||
|
||||
### BFF Controllers
|
||||
|
||||
- Controllers are thin: no business logic, no Zod imports
|
||||
- Use `createZodDto(schema)` + global `ZodValidationPipe`
|
||||
- Integrations transform data via domain mappers and return domain types
|
||||
|
||||
### Portal Features
|
||||
|
||||
- Pages under `apps/portal/src/app/**` are wrappers (no API calls)
|
||||
- Feature modules at `apps/portal/src/features/<feature>/` own:
|
||||
- `hooks/` (React Query)
|
||||
- `services/` (API calls)
|
||||
- `components/` and `views/` (UI)
|
||||
|
||||
## Code Standards
|
||||
|
||||
- No `any` in public APIs
|
||||
- No `console.log` in production (use logger)
|
||||
- TypeScript strict mode enabled
|
||||
- Always read docs before guessing endpoint behavior:
|
||||
- `docs/development/` for BFF/Portal/Domain patterns
|
||||
- `docs/architecture/` for system boundaries
|
||||
- `docs/integrations/` for external API details
|
||||
@ -1,3 +1,8 @@
|
||||
Timestamp,API Endpoint,API Method,Phone Number,SIM Identifier,Request Payload,Response Status,Error,Additional Info
|
||||
2026-01-19T04:05:41.856Z,/mvno/getTrafficInfo/,POST,02000524104652,02000524104652,"{""account"":""02000524104652""}",Success,,OK
|
||||
2026-01-19T04:05:41.945Z,/mvno/getTrafficInfo/,POST,02000524104652,02000524104652,"{""account"":""02000524104652""}",Success,,OK
|
||||
2026-01-20T08:29:56.809Z,/mvno/getTrafficInfo/,POST,02000524104652,02000524104652,"{""account"":""02000524104652""}",Success,,OK
|
||||
2026-01-20T08:29:56.956Z,/mvno/getTrafficInfo/,POST,02000524104652,02000524104652,"{""account"":""02000524104652""}",Success,,OK
|
||||
2026-01-29T05:54:55.030Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
||||
2026-01-29T05:54:59.051Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
||||
2026-01-29T05:55:03.587Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
||||
|
||||
|
@ -217,3 +217,38 @@
|
||||
font-family: var(--font-geist-sans, system-ui), system-ui, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
ANIMATIONS
|
||||
============================================================================= */
|
||||
|
||||
/* Floating blob animation for hero background */
|
||||
@keyframes blob-float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(10px, -10px) scale(1.02);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-5px, 5px) scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-blob-float {
|
||||
animation: blob-float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Animation delay utilities for staggered effects */
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.animation-delay-6000 {
|
||||
animation-delay: 6s;
|
||||
}
|
||||
|
||||
@ -7,12 +7,23 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Logo } from "@/components/atoms/logo";
|
||||
import { SiteFooter } from "@/components/organisms/SiteFooter";
|
||||
import { useAuthStore } from "@/features/auth/stores/auth.store";
|
||||
import { Wifi, Smartphone, Building2, Lock, Wrench, ChevronDown, ArrowRight } from "lucide-react";
|
||||
import {
|
||||
Wifi,
|
||||
Smartphone,
|
||||
Building2,
|
||||
Lock,
|
||||
Wrench,
|
||||
ChevronDown,
|
||||
ArrowRight,
|
||||
Menu,
|
||||
X,
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface PublicShellProps {
|
||||
children: ReactNode;
|
||||
@ -23,6 +34,9 @@ export function PublicShell({ children }: PublicShellProps) {
|
||||
const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth);
|
||||
const checkAuth = useAuthStore(state => state.checkAuth);
|
||||
const [servicesOpen, setServicesOpen] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
||||
const servicesDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasCheckedAuth) {
|
||||
@ -30,8 +44,115 @@ export function PublicShell({ children }: PublicShellProps) {
|
||||
}
|
||||
}, [checkAuth, hasCheckedAuth]);
|
||||
|
||||
// Detect touch device
|
||||
useEffect(() => {
|
||||
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
||||
}, []);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
if (!servicesOpen) return;
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
servicesDropdownRef.current &&
|
||||
!servicesDropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setServicesOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [servicesOpen]);
|
||||
|
||||
// Close mobile menu when route changes or on escape key
|
||||
useEffect(() => {
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
setMobileMenuOpen(false);
|
||||
setServicesOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, []);
|
||||
|
||||
// Prevent body scroll when mobile menu is open
|
||||
useEffect(() => {
|
||||
if (mobileMenuOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [mobileMenuOpen]);
|
||||
|
||||
const handleServicesClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (isTouchDevice) {
|
||||
e.preventDefault();
|
||||
setServicesOpen(prev => !prev);
|
||||
}
|
||||
},
|
||||
[isTouchDevice]
|
||||
);
|
||||
|
||||
const closeMobileMenu = useCallback(() => {
|
||||
setMobileMenuOpen(false);
|
||||
setServicesOpen(false);
|
||||
}, []);
|
||||
|
||||
const serviceItems = [
|
||||
{
|
||||
href: "/services/internet",
|
||||
label: "Internet Plans",
|
||||
desc: "NTT Fiber up to 10Gbps",
|
||||
icon: <Wifi className="h-5 w-5" />,
|
||||
color: "bg-sky-50 text-sky-600",
|
||||
},
|
||||
{
|
||||
href: "/services/sim",
|
||||
label: "Phone Plans",
|
||||
desc: "Docomo network SIM cards",
|
||||
icon: <Smartphone className="h-5 w-5" />,
|
||||
color: "bg-emerald-50 text-emerald-600",
|
||||
},
|
||||
{
|
||||
href: "/services/business",
|
||||
label: "Business Solutions",
|
||||
desc: "Enterprise IT services",
|
||||
icon: <Building2 className="h-5 w-5" />,
|
||||
color: "bg-violet-50 text-violet-600",
|
||||
},
|
||||
{
|
||||
href: "/services/vpn",
|
||||
label: "VPN Service",
|
||||
desc: "US & UK server access",
|
||||
icon: <Lock className="h-5 w-5" />,
|
||||
color: "bg-amber-50 text-amber-600",
|
||||
},
|
||||
{
|
||||
href: "/services/onsite",
|
||||
label: "Onsite Support",
|
||||
desc: "Tech help at your location",
|
||||
icon: <Wrench className="h-5 w-5" />,
|
||||
color: "bg-slate-100 text-slate-600",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background text-foreground">
|
||||
{/* Skip to main content link for accessibility */}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-md"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
{/* Subtle background pattern - clean and minimal */}
|
||||
<div className="fixed inset-0 -z-10 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/5 via-background to-background" />
|
||||
|
||||
@ -48,17 +169,20 @@ export function PublicShell({ children }: PublicShellProps) {
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center justify-center gap-1 sm:gap-3 text-sm font-semibold text-muted-foreground">
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center justify-center gap-1 sm:gap-3 text-sm font-semibold text-muted-foreground">
|
||||
<div
|
||||
ref={servicesDropdownRef}
|
||||
className="relative"
|
||||
onMouseEnter={() => setServicesOpen(true)}
|
||||
onMouseLeave={() => setServicesOpen(false)}
|
||||
onFocus={() => setServicesOpen(true)}
|
||||
onBlur={() => setServicesOpen(false)}
|
||||
onMouseEnter={() => !isTouchDevice && setServicesOpen(true)}
|
||||
onMouseLeave={() => !isTouchDevice && setServicesOpen(false)}
|
||||
>
|
||||
<Link
|
||||
href="/services"
|
||||
onClick={handleServicesClick}
|
||||
className="inline-flex items-center gap-1 px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
||||
aria-expanded={servicesOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
Services
|
||||
<ChevronDown
|
||||
@ -80,46 +204,11 @@ export function PublicShell({ children }: PublicShellProps) {
|
||||
|
||||
{/* Services Grid */}
|
||||
<div className="p-3 grid grid-cols-2 gap-1">
|
||||
{[
|
||||
{
|
||||
href: "/services/internet",
|
||||
label: "Internet Plans",
|
||||
desc: "NTT Fiber up to 10Gbps",
|
||||
icon: <Wifi className="h-5 w-5" />,
|
||||
color: "bg-sky-50 text-sky-600",
|
||||
},
|
||||
{
|
||||
href: "/services/sim",
|
||||
label: "Phone Plans",
|
||||
desc: "Docomo network SIM cards",
|
||||
icon: <Smartphone className="h-5 w-5" />,
|
||||
color: "bg-emerald-50 text-emerald-600",
|
||||
},
|
||||
{
|
||||
href: "/services/business",
|
||||
label: "Business Solutions",
|
||||
desc: "Enterprise IT services",
|
||||
icon: <Building2 className="h-5 w-5" />,
|
||||
color: "bg-violet-50 text-violet-600",
|
||||
},
|
||||
{
|
||||
href: "/services/vpn",
|
||||
label: "VPN Service",
|
||||
desc: "US & UK server access",
|
||||
icon: <Lock className="h-5 w-5" />,
|
||||
color: "bg-amber-50 text-amber-600",
|
||||
},
|
||||
{
|
||||
href: "/services/onsite",
|
||||
label: "Onsite Support",
|
||||
desc: "Tech help at your location",
|
||||
icon: <Wrench className="h-5 w-5" />,
|
||||
color: "bg-slate-100 text-slate-600",
|
||||
},
|
||||
].map(item => (
|
||||
{serviceItems.map(item => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setServicesOpen(false)}
|
||||
className="group flex items-start gap-3 rounded-xl p-3 hover:bg-muted/50 transition-all duration-150"
|
||||
>
|
||||
<div
|
||||
@ -143,6 +232,7 @@ export function PublicShell({ children }: PublicShellProps) {
|
||||
<div className="px-5 py-3 bg-muted/30 border-t border-border/30">
|
||||
<Link
|
||||
href="/services"
|
||||
onClick={() => setServicesOpen(false)}
|
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary hover:text-primary/80 transition-colors"
|
||||
>
|
||||
View all services
|
||||
@ -155,13 +245,13 @@ export function PublicShell({ children }: PublicShellProps) {
|
||||
</div>
|
||||
<Link
|
||||
href="/about"
|
||||
className="hidden sm:inline-flex items-center px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
||||
className="inline-flex items-center px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="hidden sm:inline-flex items-center px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
||||
className="inline-flex items-center px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
@ -173,26 +263,129 @@ export function PublicShell({ children }: PublicShellProps) {
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{isAuthenticated ? (
|
||||
<Link
|
||||
href="/account"
|
||||
className="justify-self-end inline-flex items-center justify-center rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow hover:bg-primary/90 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
{/* Mobile: Language indicator + hamburger */}
|
||||
<div className="flex md:hidden items-center justify-center">
|
||||
<div className="flex items-center gap-1 text-sm text-muted-foreground mr-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
<span className="font-medium">EN</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 justify-self-end">
|
||||
{/* Language Selector - Desktop */}
|
||||
<div className="hidden md:flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<Globe className="h-4 w-4" />
|
||||
<span className="font-medium">EN</span>
|
||||
</div>
|
||||
|
||||
{/* Auth Button - Desktop */}
|
||||
{isAuthenticated ? (
|
||||
<Link
|
||||
href="/account"
|
||||
className="hidden md:inline-flex items-center justify-center rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow hover:bg-primary/90 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
My Account
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="hidden md:inline-flex items-center justify-center rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow hover:bg-primary/90 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="md:hidden inline-flex items-center justify-center w-10 h-10 rounded-md hover:bg-muted/50 transition-colors"
|
||||
aria-expanded={mobileMenuOpen}
|
||||
aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
|
||||
>
|
||||
My Account
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="justify-self-end inline-flex items-center justify-center rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow hover:bg-primary/90 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
)}
|
||||
{mobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1">
|
||||
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] pt-0 pb-12 sm:pb-16">
|
||||
{/* Mobile Menu Overlay - Rendered outside header to avoid stacking context issues */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden fixed inset-0 top-16 z-50 bg-white animate-in fade-in duration-200 overflow-y-auto">
|
||||
<nav className="flex flex-col p-6 space-y-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-semibold text-primary uppercase tracking-wider px-3 py-2">
|
||||
Services
|
||||
</p>
|
||||
{serviceItems.map(item => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={closeMobileMenu}
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-lg ${item.color}`}
|
||||
>
|
||||
{item.icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">{item.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{item.desc}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/40 pt-4 space-y-1">
|
||||
<Link
|
||||
href="/about"
|
||||
onClick={closeMobileMenu}
|
||||
className="flex items-center px-3 py-3 rounded-lg text-base font-semibold text-foreground hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog"
|
||||
onClick={closeMobileMenu}
|
||||
className="flex items-center px-3 py-3 rounded-lg text-base font-semibold text-foreground hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
onClick={closeMobileMenu}
|
||||
className="flex items-center px-3 py-3 rounded-lg text-base font-semibold text-foreground hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
Support
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/40 pt-4">
|
||||
{isAuthenticated ? (
|
||||
<Link
|
||||
href="/account"
|
||||
onClick={closeMobileMenu}
|
||||
className="flex items-center justify-center rounded-full bg-primary px-6 py-3 text-base font-semibold text-primary-foreground shadow hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
My Account
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href="/auth/login"
|
||||
onClick={closeMobileMenu}
|
||||
className="flex items-center justify-center rounded-full bg-primary px-6 py-3 text-base font-semibold text-primary-foreground shadow hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<main id="main-content" className="flex-1">
|
||||
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] pt-0 pb-0">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -267,18 +267,31 @@ export function AboutUsView() {
|
||||
|
||||
{/* Corporate Data Section */}
|
||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-12">
|
||||
<div className="max-w-6xl mx-auto px-6 sm:px-8 space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="max-w-6xl mx-auto px-6 sm:px-8 space-y-3">
|
||||
{/* Row 1: headings same level */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1.2fr_0.8fr] gap-x-10 gap-y-2 items-start">
|
||||
<h2 className="text-2xl font-bold text-foreground">Corporate Data</h2>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">Address</h3>
|
||||
<p className="text-muted-foreground font-semibold leading-relaxed">
|
||||
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu,
|
||||
<br />
|
||||
Minato-ku, Tokyo 106-0044
|
||||
<br />
|
||||
Tel: 03-3560-1006 Fax: 03-3560-1007
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1.2fr_0.8fr] gap-10 items-start">
|
||||
<div className="space-y-5">
|
||||
|
||||
{/* Row 2: corporate data list | map (no stretch, no extra space below left column) */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1.2fr_0.8fr] gap-x-10 gap-y-2 items-start">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
|
||||
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||
Representative Director
|
||||
</h3>
|
||||
<p className="text-muted-foreground font-semibold mt-1">Daisuke Nagakawa</p>
|
||||
<p className="text-muted-foreground font-semibold mt-0.5">Daisuke Nagakawa</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -286,7 +299,7 @@ export function AboutUsView() {
|
||||
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||
Employees
|
||||
</h3>
|
||||
<p className="text-muted-foreground font-semibold mt-1">
|
||||
<p className="text-muted-foreground font-semibold mt-0.5">
|
||||
21 Staff Members (as of March 31st, 2025)
|
||||
</p>
|
||||
</div>
|
||||
@ -296,7 +309,7 @@ export function AboutUsView() {
|
||||
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||
Established
|
||||
</h3>
|
||||
<p className="text-muted-foreground font-semibold mt-1">March 8, 2002</p>
|
||||
<p className="text-muted-foreground font-semibold mt-0.5">March 8, 2002</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -304,7 +317,7 @@ export function AboutUsView() {
|
||||
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||
Paid in Capital
|
||||
</h3>
|
||||
<p className="text-muted-foreground font-semibold mt-1">40,000,000 JPY</p>
|
||||
<p className="text-muted-foreground font-semibold mt-0.5">40,000,000 JPY</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -312,7 +325,7 @@ export function AboutUsView() {
|
||||
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||
Business Hours
|
||||
</h3>
|
||||
<div className="text-muted-foreground font-semibold mt-1 space-y-1">
|
||||
<div className="text-muted-foreground font-semibold mt-0.5 space-y-0.5">
|
||||
<p>Mon - Fri 9:30AM - 6:00PM — Customer Support Team</p>
|
||||
<p>Mon - Fri 9:30AM - 6:00PM — In-office Tech Support Team</p>
|
||||
<p>Mon - Sat 10:00AM - 9:00PM — Onsite Tech Support Team</p>
|
||||
@ -320,27 +333,15 @@ export function AboutUsView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-muted/20 rounded-2xl border border-border/60 p-4">
|
||||
<h3 className="text-xl font-bold text-foreground mb-2 text-center">Address</h3>
|
||||
<p className="text-center text-muted-foreground font-semibold leading-relaxed">
|
||||
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu,
|
||||
<br />
|
||||
Minato-ku, Tokyo 106-0044
|
||||
<br />
|
||||
Tel: 03-3560-1006 Fax: 03-3560-1007
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl overflow-hidden shadow-md border border-border/70">
|
||||
<iframe
|
||||
title="Assist Solutions Corp Map"
|
||||
src="https://www.google.com/maps?q=Assist+Solutions+Corp,+3-8-2+Higashi+Azabu,+Minato-ku,+Tokyo&output=embed"
|
||||
className="w-full h-[320px]"
|
||||
loading="lazy"
|
||||
allowFullScreen
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-2xl overflow-hidden w-full min-h-[320px]">
|
||||
<iframe
|
||||
title="Assist Solutions Corp Map"
|
||||
src="https://www.google.com/maps?q=Assist+Solutions+Corp,+3-8-2+Higashi+Azabu,+Minato-ku,+Tokyo&output=embed"
|
||||
className="w-full h-[320px] block"
|
||||
loading="lazy"
|
||||
allowFullScreen
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -345,16 +345,10 @@ function ExpandedTierDetails({
|
||||
config,
|
||||
tiers,
|
||||
setupFee,
|
||||
ctaPath,
|
||||
ctaLabel,
|
||||
onCtaClick,
|
||||
}: {
|
||||
config: OfferingTypeConfig;
|
||||
tiers: TierInfo[];
|
||||
setupFee: number;
|
||||
ctaPath: string;
|
||||
ctaLabel: string;
|
||||
onCtaClick?: (e: React.MouseEvent) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden">
|
||||
@ -455,23 +449,14 @@ function ExpandedTierDetails({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer with setup fee and CTA */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 pt-5 mt-5 border-t border-border">
|
||||
{/* Footer with setup fee */}
|
||||
<div className="pt-5 mt-5 border-t border-border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="font-semibold text-foreground">
|
||||
+ ¥{setupFee.toLocaleString()} one-time setup
|
||||
</span>{" "}
|
||||
(or 12/24-month installment)
|
||||
</p>
|
||||
{onCtaClick ? (
|
||||
<Button onClick={onCtaClick} size="lg" className="whitespace-nowrap">
|
||||
{ctaLabel}
|
||||
</Button>
|
||||
) : (
|
||||
<Button as="a" href={ctaPath} size="lg" className="whitespace-nowrap">
|
||||
{ctaLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -484,24 +469,14 @@ function ExpandedTierDetails({
|
||||
function AvailablePlansSection({
|
||||
plansByOffering,
|
||||
setupFee,
|
||||
ctaPath,
|
||||
ctaLabel,
|
||||
onCtaClick,
|
||||
}: {
|
||||
plansByOffering: Record<string, { minPrice: number; tiers: TierInfo[] }>;
|
||||
setupFee: number;
|
||||
ctaPath: string;
|
||||
ctaLabel: string;
|
||||
onCtaClick?: (e: React.MouseEvent) => void;
|
||||
}) {
|
||||
const [expandedCards, setExpandedCards] = useState<Record<string, boolean>>({
|
||||
home10g: false,
|
||||
home1g: false,
|
||||
apartment: false,
|
||||
});
|
||||
const [expandedCard, setExpandedCard] = useState<string | null>(null);
|
||||
|
||||
const toggleCard = (id: string) => {
|
||||
setExpandedCards(prev => ({ ...prev, [id]: !prev[id] }));
|
||||
setExpandedCard(prev => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
// Get configs that have data
|
||||
@ -522,7 +497,7 @@ function AvailablePlansSection({
|
||||
key={config.id}
|
||||
config={config}
|
||||
minPrice={offeringData.minPrice}
|
||||
isExpanded={expandedCards[config.id] ?? false}
|
||||
isExpanded={expandedCard === config.id}
|
||||
onToggle={() => toggleCard(config.id)}
|
||||
/>
|
||||
);
|
||||
@ -533,7 +508,7 @@ function AvailablePlansSection({
|
||||
<div className="space-y-4">
|
||||
{availableConfigs.map(config => {
|
||||
const offeringData = plansByOffering[config.id];
|
||||
if (!offeringData || !expandedCards[config.id]) return null;
|
||||
if (!offeringData || expandedCard !== config.id) return null;
|
||||
|
||||
return (
|
||||
<ExpandedTierDetails
|
||||
@ -541,9 +516,6 @@ function AvailablePlansSection({
|
||||
config={config}
|
||||
tiers={offeringData.tiers}
|
||||
setupFee={setupFee}
|
||||
ctaPath={ctaPath}
|
||||
ctaLabel={ctaLabel}
|
||||
onCtaClick={onCtaClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -816,9 +788,6 @@ export function PublicInternetPlansContent({
|
||||
<AvailablePlansSection
|
||||
plansByOffering={plansByOffering}
|
||||
setupFee={consolidatedPlanData.setupFee}
|
||||
ctaPath={ctaPath}
|
||||
ctaLabel={ctaLabel}
|
||||
onCtaClick={onCtaClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -115,7 +115,7 @@ export function PublicContactView() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 pb-16">
|
||||
<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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user