Enhance Portal Metadata and CSP Configuration
- Updated metadata for various pages in the portal, improving SEO and user engagement with more descriptive titles and keywords. - Added structured data for organization in the layout component to enhance search visibility. - Configured Content Security Policy (CSP) to allow frame sources from Google, improving security and functionality for embedded content.
This commit is contained in:
parent
08511ec2b4
commit
d4b34faeb4
BIN
apps/portal/public/assets/images/About us.png
Normal file
BIN
apps/portal/public/assets/images/About us.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
@ -4,8 +4,27 @@
|
|||||||
* Corporate profile and company information.
|
* Corporate profile and company information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { AboutUsView } from "@/features/marketing/views/AboutUsView";
|
import { AboutUsView } from "@/features/marketing/views/AboutUsView";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "About Us - Assist Solutions Corp. | Company Profile",
|
||||||
|
description:
|
||||||
|
"Learn about Assist Solutions Corp., established in 2002. A registered telecom carrier (A-19-9538) providing IT services to Japan's international community with bilingual support.",
|
||||||
|
keywords: [
|
||||||
|
"Assist Solutions",
|
||||||
|
"IT company Tokyo",
|
||||||
|
"expat services Japan",
|
||||||
|
"telecom carrier Japan",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "About Assist Solutions Corp.",
|
||||||
|
description:
|
||||||
|
"Established in 2002, serving Japan's international community with reliable IT solutions and English support.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
return <AboutUsView />;
|
return <AboutUsView />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,21 @@
|
|||||||
* Contact form for unauthenticated users.
|
* Contact form for unauthenticated users.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { PublicContactView } from "@/features/support/views/PublicContactView";
|
import { PublicContactView } from "@/features/support/views/PublicContactView";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Contact Us - Assist Solutions | Get Support",
|
||||||
|
description:
|
||||||
|
"Contact Assist Solutions for internet, mobile, and IT support inquiries. Call toll-free 0120-660-470 or reach us online. English and Japanese support available.",
|
||||||
|
keywords: ["contact Assist Solutions", "IT support Tokyo", "customer service"],
|
||||||
|
openGraph: {
|
||||||
|
title: "Contact Assist Solutions",
|
||||||
|
description: "Get in touch with our bilingual support team. Toll-free: 0120-660-470",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function ContactPage() {
|
export default function ContactPage() {
|
||||||
return <PublicContactView />;
|
return <PublicContactView />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,27 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import { PublicLandingView } from "@/features/landing-page";
|
import { PublicLandingView } from "@/features/landing-page";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Assist Solutions - Internet, Mobile & IT Services for Expats in Japan",
|
||||||
|
description:
|
||||||
|
"One stop IT solution for Japan's international community. Reliable fiber internet, mobile SIM cards, VPN, TV services and bilingual tech support since 2002.",
|
||||||
|
keywords: [
|
||||||
|
"internet Japan",
|
||||||
|
"expat internet Tokyo",
|
||||||
|
"SIM card Japan",
|
||||||
|
"English support IT",
|
||||||
|
"fiber optic Japan",
|
||||||
|
"VPN Japan",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "Assist Solutions - IT Services for Expats in Japan",
|
||||||
|
description:
|
||||||
|
"Reliable internet, mobile, VPN and tech support with English service. Serving Japan's international community since 2002.",
|
||||||
|
type: "website",
|
||||||
|
locale: "en_US",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function PublicHomePage() {
|
export default function PublicHomePage() {
|
||||||
return <PublicLandingView />;
|
return <PublicLandingView />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,26 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import { Button } from "@/components/atoms";
|
import { Button } from "@/components/atoms";
|
||||||
import { Server, Monitor, Wrench, Globe } from "lucide-react";
|
import { Server, Monitor, Wrench, Globe } from "lucide-react";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Business Solutions - Enterprise IT Services | Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Comprehensive business IT solutions: Dedicated Internet Access with SLA, office LAN setup, data center services at Equinix Tokyo, and ongoing tech support.",
|
||||||
|
keywords: [
|
||||||
|
"business internet Japan",
|
||||||
|
"dedicated internet access",
|
||||||
|
"office LAN setup Tokyo",
|
||||||
|
"data center Japan",
|
||||||
|
"enterprise IT Tokyo",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "Business Solutions - Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Enterprise-grade IT services with dedicated bandwidth, data center hosting, and bilingual support.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function BusinessSolutionsPage() {
|
export default function BusinessSolutionsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto px-4">
|
<div className="max-w-6xl mx-auto px-4">
|
||||||
@ -75,6 +95,58 @@ export default function BusinessSolutionsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
<div className="max-w-4xl mx-auto mb-16">
|
||||||
|
<h2 className="text-3xl font-bold text-foreground mb-8 text-center">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-card rounded-2xl border border-border/60 p-8">
|
||||||
|
<h3 className="text-xl font-bold text-foreground mb-3">
|
||||||
|
What is Dedicated Internet Access (DIA) and how is it different from regular internet?
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
DIA provides a dedicated, unshared connection with guaranteed bandwidth and uptime
|
||||||
|
SLA. Unlike shared business internet, your speeds are consistent regardless of network
|
||||||
|
congestion, making it ideal for businesses with critical online operations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-2xl border border-border/60 p-8">
|
||||||
|
<h3 className="text-xl font-bold text-foreground mb-3">
|
||||||
|
Do you offer support contracts for ongoing IT maintenance?
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
Yes, we offer flexible support contracts ranging from ad-hoc support to comprehensive
|
||||||
|
managed IT services. Our team can handle network monitoring, security updates, and
|
||||||
|
regular maintenance to keep your systems running smoothly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-2xl border border-border/60 p-8">
|
||||||
|
<h3 className="text-xl font-bold text-foreground mb-3">
|
||||||
|
Can you set up a network for a new office or relocating business?
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
Absolutely. We specialize in complete office network setups including cable
|
||||||
|
installation, switch configuration, firewall setup, and Wi-Fi deployment. We can also
|
||||||
|
coordinate with NTT and building management for new line installations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-2xl border border-border/60 p-8">
|
||||||
|
<h3 className="text-xl font-bold text-foreground mb-3">
|
||||||
|
What data center facilities do you use?
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
We partner with Equinix (Tokyo Tennozu Isle) and GDC (Gotenyama) for colocation and
|
||||||
|
data center services. Both facilities offer enterprise-grade security, power
|
||||||
|
redundancy, and connectivity options.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<div className="text-center py-12 bg-muted/20 rounded-3xl mb-16">
|
<div className="text-center py-12 bg-muted/20 rounded-3xl mb-16">
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-4">
|
<h2 className="text-2xl font-bold text-foreground mb-4">
|
||||||
|
|||||||
@ -4,9 +4,29 @@
|
|||||||
* Displays internet plans for unauthenticated users.
|
* Displays internet plans for unauthenticated users.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { PublicInternetPlansView } from "@/features/services/views/PublicInternetPlans";
|
import { PublicInternetPlansView } from "@/features/services/views/PublicInternetPlans";
|
||||||
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
|
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Internet Plans - Fiber Optic Internet in Japan | Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Get reliable NTT fiber optic internet up to 10Gbps with English support. IPv6/IPoE connections, no long-term contracts, monthly billing. Serving expats in Tokyo.",
|
||||||
|
keywords: [
|
||||||
|
"fiber internet Japan",
|
||||||
|
"NTT fiber optic",
|
||||||
|
"internet for expats",
|
||||||
|
"10Gbps internet Tokyo",
|
||||||
|
"IPv6 Japan",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "Internet Plans - Assist Solutions",
|
||||||
|
description:
|
||||||
|
"High-speed NTT fiber optic internet with English support. Up to 10Gbps, no long-term contracts.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function PublicInternetPlansPage() {
|
export default function PublicInternetPlansPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,6 +1,25 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import { Button } from "@/components/atoms";
|
import { Button } from "@/components/atoms";
|
||||||
import { Users, Monitor, Tv, Headset } from "lucide-react";
|
import { Users, Monitor, Tv, Headset } from "lucide-react";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Onsite Support - Tech Support in Tokyo | Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Professional onsite and remote tech support in Tokyo, Saitama, and Kanagawa. Wi-Fi setup, network troubleshooting, computer repair. English-speaking technicians.",
|
||||||
|
keywords: [
|
||||||
|
"tech support Tokyo",
|
||||||
|
"IT support expats",
|
||||||
|
"computer repair Japan",
|
||||||
|
"Wi-Fi setup Tokyo",
|
||||||
|
"English IT support",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "Onsite Tech Support - Assist Solutions",
|
||||||
|
description: "English-speaking technicians for home and office IT support in Tokyo area.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function OnsiteSupportPage() {
|
export default function OnsiteSupportPage() {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto px-4">
|
<div className="max-w-6xl mx-auto px-4">
|
||||||
|
|||||||
@ -1,5 +1,24 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import { ServicesGrid } from "@/features/services/components/common/ServicesGrid";
|
import { ServicesGrid } from "@/features/services/components/common/ServicesGrid";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Services - Internet, Mobile, VPN & IT Support | Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Explore our comprehensive IT services: fiber optic internet up to 10Gbps, mobile SIM cards, VPN access, TV services, and professional onsite support in Tokyo.",
|
||||||
|
keywords: [
|
||||||
|
"internet service Japan",
|
||||||
|
"SIM card Tokyo",
|
||||||
|
"VPN service",
|
||||||
|
"IT support expats",
|
||||||
|
"fiber internet",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "Our Services - Assist Solutions",
|
||||||
|
description: "Internet, mobile, VPN, TV and tech support services for expats in Japan.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
interface ServicesPageProps {
|
interface ServicesPageProps {
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,29 @@
|
|||||||
* Displays SIM plans for unauthenticated users.
|
* Displays SIM plans for unauthenticated users.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { PublicSimPlansView } from "@/features/services/views/PublicSimPlans";
|
import { PublicSimPlansView } from "@/features/services/views/PublicSimPlans";
|
||||||
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
|
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "SIM Card Plans - Mobile Service in Japan | Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Get affordable SIM cards using NTT Docomo's network. Voice + Data and Data-only plans, eSIM available. First month free, no credit check for foreigners.",
|
||||||
|
keywords: [
|
||||||
|
"SIM card Japan",
|
||||||
|
"mobile service expats",
|
||||||
|
"Docomo MVNO",
|
||||||
|
"eSIM Japan",
|
||||||
|
"prepaid SIM Tokyo",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "Mobile SIM Card Plans - Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Affordable mobile plans on Docomo's network with English support. SIM and eSIM available.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function PublicSimPlansPage() {
|
export default function PublicSimPlansPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import { Button } from "@/components/atoms";
|
import { Button } from "@/components/atoms";
|
||||||
import {
|
import {
|
||||||
Tv,
|
Tv,
|
||||||
@ -11,6 +12,25 @@ import {
|
|||||||
Globe,
|
Globe,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "TV Services - Satellite & Cable TV in Japan | Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Access premium TV services including Sky PerfecTV, iTSCOM, and J:COM. We arrange your TV service at no extra fee. English support available.",
|
||||||
|
keywords: [
|
||||||
|
"cable TV Japan",
|
||||||
|
"satellite TV Tokyo",
|
||||||
|
"Sky PerfecTV",
|
||||||
|
"JCOM English",
|
||||||
|
"TV service expats",
|
||||||
|
],
|
||||||
|
openGraph: {
|
||||||
|
title: "TV Services - Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Premium cable and satellite TV options in Japan. Free consultation and arrangement service.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function TVServicesPage() {
|
export default function TVServicesPage() {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto px-4">
|
<div className="max-w-6xl mx-auto px-4">
|
||||||
|
|||||||
@ -4,9 +4,23 @@
|
|||||||
* Displays VPN plans for unauthenticated users.
|
* Displays VPN plans for unauthenticated users.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { PublicVpnPlansView } from "@/features/services/views/PublicVpnPlans";
|
import { PublicVpnPlansView } from "@/features/services/views/PublicVpnPlans";
|
||||||
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
|
import { RedirectAuthenticatedToAccountServices } from "@/features/services/components/common/RedirectAuthenticatedToAccountServices";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "VPN Service - Secure VPN Access from Japan | Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Reliable VPN service with servers in San Francisco and London. Static routing from Tokyo, optional VPN Wi-Fi router rental. Perfect for streaming and privacy.",
|
||||||
|
keywords: ["VPN Japan", "VPN service Tokyo", "streaming VPN", "secure internet Japan"],
|
||||||
|
openGraph: {
|
||||||
|
title: "VPN Service - Assist Solutions",
|
||||||
|
description:
|
||||||
|
"Secure VPN access from Tokyo with servers in US and UK. Static routing and router rental available.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function PublicVpnPlansPage() {
|
export default function PublicVpnPlansPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -5,8 +5,65 @@ import { QueryProvider } from "@/core/providers";
|
|||||||
import { SessionTimeoutWarning } from "@/features/auth/components/SessionTimeoutWarning";
|
import { SessionTimeoutWarning } from "@/features/auth/components/SessionTimeoutWarning";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Assist Solutions Portal",
|
title: {
|
||||||
description: "Manage your subscriptions, billing, and support with Assist Solutions",
|
default: "Assist Solutions - IT Services for Expats in Japan",
|
||||||
|
template: "%s | Assist Solutions",
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"One stop IT solution for Japan's international community. Internet, mobile, VPN, and tech support with English service since 2002.",
|
||||||
|
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || "https://portal.asolutions.co.jp"),
|
||||||
|
alternates: {
|
||||||
|
canonical: "/",
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
locale: "en_US",
|
||||||
|
siteName: "Assist Solutions",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Organization structured data for rich search results
|
||||||
|
const organizationJsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "Assist Solutions Corp.",
|
||||||
|
alternateName: "Assist Solutions",
|
||||||
|
url: "https://asolutions.co.jp",
|
||||||
|
logo: "https://portal.asolutions.co.jp/assets/images/logo.png",
|
||||||
|
foundingDate: "2002-03-08",
|
||||||
|
description:
|
||||||
|
"IT and telecom services for Japan's international community with bilingual English/Japanese support.",
|
||||||
|
address: {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
streetAddress: "3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu",
|
||||||
|
addressLocality: "Minato-ku",
|
||||||
|
addressRegion: "Tokyo",
|
||||||
|
postalCode: "106-0044",
|
||||||
|
addressCountry: "JP",
|
||||||
|
},
|
||||||
|
contactPoint: [
|
||||||
|
{
|
||||||
|
"@type": "ContactPoint",
|
||||||
|
telephone: "+81-3-3560-1006",
|
||||||
|
contactType: "customer service",
|
||||||
|
availableLanguage: ["English", "Japanese"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ContactPoint",
|
||||||
|
telephone: "0120-660-470",
|
||||||
|
contactType: "customer service",
|
||||||
|
areaServed: "JP",
|
||||||
|
availableLanguage: ["English", "Japanese"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sameAs: ["https://www.asolutions.co.jp"],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disable static generation for the entire app since it uses dynamic features extensively
|
// Disable static generation for the entire app since it uses dynamic features extensively
|
||||||
@ -24,6 +81,12 @@ export default async function RootLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
<body className="antialiased">
|
<body className="antialiased">
|
||||||
<QueryProvider nonce={nonce}>
|
<QueryProvider nonce={nonce}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
22
apps/portal/src/app/robots.ts
Normal file
22
apps/portal/src/app/robots.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Robots.txt configuration
|
||||||
|
*
|
||||||
|
* Controls search engine crawler access.
|
||||||
|
* Allows all public pages, blocks account/authenticated areas.
|
||||||
|
*/
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://portal.asolutions.co.jp";
|
||||||
|
|
||||||
|
return {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
userAgent: "*",
|
||||||
|
allow: "/",
|
||||||
|
disallow: ["/account/", "/api/", "/auth/", "/_next/", "/order/"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sitemap: `${baseUrl}/sitemap.xml`,
|
||||||
|
};
|
||||||
|
}
|
||||||
33
apps/portal/src/app/sitemap.ts
Normal file
33
apps/portal/src/app/sitemap.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sitemap for SEO
|
||||||
|
*
|
||||||
|
* Generates a sitemap.xml for search engine crawlers.
|
||||||
|
* Only includes public pages that should be indexed.
|
||||||
|
*/
|
||||||
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://portal.asolutions.co.jp";
|
||||||
|
|
||||||
|
// Public pages that should be indexed
|
||||||
|
const publicPages = [
|
||||||
|
{ path: "", priority: 1.0, changeFrequency: "weekly" as const },
|
||||||
|
{ path: "/about", priority: 0.8, changeFrequency: "monthly" as const },
|
||||||
|
{ path: "/services", priority: 0.9, changeFrequency: "weekly" as const },
|
||||||
|
{ path: "/services/internet", priority: 0.8, changeFrequency: "weekly" as const },
|
||||||
|
{ path: "/services/sim", priority: 0.8, changeFrequency: "weekly" as const },
|
||||||
|
{ path: "/services/vpn", priority: 0.7, changeFrequency: "monthly" as const },
|
||||||
|
{ path: "/services/tv", priority: 0.7, changeFrequency: "monthly" as const },
|
||||||
|
{ path: "/services/onsite", priority: 0.7, changeFrequency: "monthly" as const },
|
||||||
|
{ path: "/services/business", priority: 0.7, changeFrequency: "monthly" as const },
|
||||||
|
{ path: "/contact", priority: 0.8, changeFrequency: "monthly" as const },
|
||||||
|
{ path: "/help", priority: 0.6, changeFrequency: "monthly" as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
return publicPages.map(page => ({
|
||||||
|
url: `${baseUrl}${page.path}`,
|
||||||
|
lastModified: new Date(),
|
||||||
|
changeFrequency: page.changeFrequency,
|
||||||
|
priority: page.priority,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@ -7,11 +7,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Logo } from "@/components/atoms/logo";
|
import { Logo } from "@/components/atoms/logo";
|
||||||
import { SiteFooter } from "@/components/organisms/SiteFooter";
|
import { SiteFooter } from "@/components/organisms/SiteFooter";
|
||||||
import { useAuthStore } from "@/features/auth/stores/auth.store";
|
import { useAuthStore } from "@/features/auth/stores/auth.store";
|
||||||
|
import {
|
||||||
|
Wifi,
|
||||||
|
Smartphone,
|
||||||
|
Building2,
|
||||||
|
Lock,
|
||||||
|
Tv,
|
||||||
|
Wrench,
|
||||||
|
ChevronDown,
|
||||||
|
ArrowRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export interface PublicShellProps {
|
export interface PublicShellProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -21,6 +31,7 @@ export function PublicShell({ children }: PublicShellProps) {
|
|||||||
const isAuthenticated = useAuthStore(state => state.isAuthenticated);
|
const isAuthenticated = useAuthStore(state => state.isAuthenticated);
|
||||||
const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth);
|
const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth);
|
||||||
const checkAuth = useAuthStore(state => state.checkAuth);
|
const checkAuth = useAuthStore(state => state.checkAuth);
|
||||||
|
const [servicesOpen, setServicesOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasCheckedAuth) {
|
if (!hasCheckedAuth) {
|
||||||
@ -47,12 +58,117 @@ export function PublicShell({ children }: PublicShellProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="flex items-center justify-center gap-1 sm:gap-3 text-sm font-semibold text-muted-foreground">
|
<nav className="flex items-center justify-center gap-1 sm:gap-3 text-sm font-semibold text-muted-foreground">
|
||||||
<Link
|
<div
|
||||||
href="/services"
|
className="relative"
|
||||||
className="inline-flex items-center px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
onMouseEnter={() => setServicesOpen(true)}
|
||||||
|
onMouseLeave={() => setServicesOpen(false)}
|
||||||
|
onFocus={() => setServicesOpen(true)}
|
||||||
|
onBlur={() => setServicesOpen(false)}
|
||||||
>
|
>
|
||||||
Services
|
<Link
|
||||||
</Link>
|
href="/services"
|
||||||
|
className="inline-flex items-center gap-1 px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Services
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-4 w-4 transition-transform duration-200 ${servicesOpen ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
{servicesOpen && (
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-full pt-2 z-50">
|
||||||
|
{/* Arrow pointer */}
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 -top-0 w-3 h-3 rotate-45 bg-white border-l border-t border-border/50" />
|
||||||
|
|
||||||
|
<div className="w-[420px] rounded-2xl border border-border/50 bg-white shadow-2xl overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-5 py-3 bg-gradient-to-r from-primary/5 to-transparent border-b border-border/30">
|
||||||
|
<p className="text-xs font-semibold text-primary uppercase tracking-wider">
|
||||||
|
Browse Our Services
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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/tv",
|
||||||
|
label: "TV Services",
|
||||||
|
desc: "Satellite & Cable TV",
|
||||||
|
icon: <Tv className="h-5 w-5" />,
|
||||||
|
color: "bg-rose-50 text-rose-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 => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className="group flex items-start gap-3 rounded-xl p-3 hover:bg-muted/50 transition-all duration-150"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-lg ${item.color} transition-transform group-hover:scale-110`}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 pt-0.5">
|
||||||
|
<p className="text-sm font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||||
|
{item.label}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground leading-snug">
|
||||||
|
{item.desc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer CTA */}
|
||||||
|
<div className="px-5 py-3 bg-muted/30 border-t border-border/30">
|
||||||
|
<Link
|
||||||
|
href="/services"
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary hover:text-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
View all services
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/about"
|
href="/about"
|
||||||
className="hidden sm:inline-flex items-center px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
className="hidden sm:inline-flex items-center px-3 py-2 rounded-md hover:text-foreground transition-colors"
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Wifi,
|
Wifi,
|
||||||
@ -17,8 +17,71 @@ import {
|
|||||||
MessageSquare,
|
MessageSquare,
|
||||||
PhoneCall,
|
PhoneCall,
|
||||||
Train,
|
Train,
|
||||||
|
Globe,
|
||||||
|
Server,
|
||||||
|
Shield,
|
||||||
|
Languages,
|
||||||
|
Calendar,
|
||||||
|
ShieldCheck,
|
||||||
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
type ServiceCategory = "personal" | "business";
|
||||||
|
|
||||||
|
interface ServiceItem {
|
||||||
|
title: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const personalServices: ServiceItem[] = [
|
||||||
|
{
|
||||||
|
title: "Internet Plan",
|
||||||
|
icon: <Wifi className="h-8 w-8 text-primary" />,
|
||||||
|
href: "/services/internet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Phone Plan",
|
||||||
|
icon: <Smartphone className="h-8 w-8 text-primary" />,
|
||||||
|
href: "/services/sim",
|
||||||
|
},
|
||||||
|
{ title: "VPN Service", icon: <Lock className="h-8 w-8 text-primary" />, href: "/services/vpn" },
|
||||||
|
{ title: "TV Services", icon: <Tv className="h-8 w-8 text-primary" />, href: "/services/tv" },
|
||||||
|
{
|
||||||
|
title: "Onsite Support",
|
||||||
|
icon: <Wrench className="h-8 w-8 text-primary" />,
|
||||||
|
href: "/services/onsite",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const businessServices: ServiceItem[] = [
|
||||||
|
{
|
||||||
|
title: "Office LAN Setup",
|
||||||
|
icon: <Server className="h-8 w-8 text-primary" />,
|
||||||
|
href: "/services/business",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Onsite & Remote Tech Support",
|
||||||
|
icon: <Wrench className="h-8 w-8 text-primary" />,
|
||||||
|
href: "/services/onsite",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Dedicated Internet Access",
|
||||||
|
icon: <Building2 className="h-8 w-8 text-primary" />,
|
||||||
|
href: "/services/business",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Data Center Service",
|
||||||
|
icon: <Shield className="h-8 w-8 text-primary" />,
|
||||||
|
href: "/services/business",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Website Construction & Maintenance",
|
||||||
|
icon: <Globe className="h-8 w-8 text-primary" />,
|
||||||
|
href: "/services/business",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PublicLandingView - Marketing-focused landing page
|
* PublicLandingView - Marketing-focused landing page
|
||||||
*
|
*
|
||||||
@ -30,51 +93,113 @@ import {
|
|||||||
*/
|
*/
|
||||||
export function PublicLandingView() {
|
export function PublicLandingView() {
|
||||||
const carouselRef = useRef<HTMLDivElement>(null);
|
const carouselRef = useRef<HTMLDivElement>(null);
|
||||||
|
const itemWidthRef = useRef(0);
|
||||||
|
const [activeCategory, setActiveCategory] = useState<ServiceCategory>("personal");
|
||||||
|
|
||||||
const services = [
|
const services = [
|
||||||
{
|
{
|
||||||
title: "Internet Plans",
|
title: "Internet Plans",
|
||||||
description:
|
description:
|
||||||
"Utilizing NTT's optical fiber network, we deliver one of the most reliable Internet connections in Japan.",
|
"Utilizing NTT's optical fiber network, we deliver one of the most reliable Internet connections in Japan.",
|
||||||
icon: <Wifi className="h-8 w-8 text-blue-600" />,
|
icon: <Wifi className="h-8 w-8 text-primary" />,
|
||||||
href: "/services/internet",
|
href: "/services/internet",
|
||||||
|
price: "From 5,280 JPY/mo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Phone Plans",
|
title: "Phone Plans",
|
||||||
description:
|
description:
|
||||||
"Using NTT DOCOMO's vast mobile network, we deliver one of the most cost-friendly SIM card services in Japan.",
|
"Using NTT DOCOMO's vast mobile network, we deliver one of the most cost-friendly SIM card services in Japan.",
|
||||||
icon: <Smartphone className="h-8 w-8 text-blue-600" />,
|
icon: <Smartphone className="h-8 w-8 text-primary" />,
|
||||||
href: "/services/sim",
|
href: "/services/sim",
|
||||||
|
price: "From 1,078 JPY/mo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Business Solutions",
|
title: "Business Solutions",
|
||||||
description:
|
description:
|
||||||
"Dedicated Internet Access, office network setup, data center services, and ongoing tech support.",
|
"Dedicated Internet Access, office network setup, data center services, and ongoing tech support.",
|
||||||
icon: <Building2 className="h-8 w-8 text-blue-600" />,
|
icon: <Building2 className="h-8 w-8 text-primary" />,
|
||||||
href: "/services/business",
|
href: "/services/business",
|
||||||
|
price: "Custom Quote",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "VPN",
|
title: "VPN",
|
||||||
description:
|
description:
|
||||||
"Choose any of our VPN server locations that connect directly from Tokyo with static routing.",
|
"Choose any of our VPN server locations that connect directly from Tokyo with static routing.",
|
||||||
icon: <Lock className="h-8 w-8 text-blue-600" />,
|
icon: <Lock className="h-8 w-8 text-primary" />,
|
||||||
href: "/services/vpn",
|
href: "/services/vpn",
|
||||||
|
price: "From 1,100 JPY/mo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "TV Services",
|
title: "TV Services",
|
||||||
description: "A variety of options for customers such as Satellite TV and Optical Fiber TV.",
|
description: "A variety of options for customers such as Satellite TV and Optical Fiber TV.",
|
||||||
icon: <Tv className="h-8 w-8 text-blue-600" />,
|
icon: <Tv className="h-8 w-8 text-primary" />,
|
||||||
href: "/services/tv",
|
href: "/services/tv",
|
||||||
|
price: "From 4,389 JPY/mo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Onsite Support",
|
title: "Onsite Support",
|
||||||
description:
|
description:
|
||||||
"Professional technical support at your residence or office for setup, configuration, and troubleshooting.",
|
"Professional technical support at your residence or office for setup, configuration, and troubleshooting.",
|
||||||
icon: <Wrench className="h-8 w-8 text-blue-600" />,
|
icon: <Wrench className="h-8 w-8 text-primary" />,
|
||||||
href: "/services/onsite",
|
href: "/services/onsite",
|
||||||
|
price: "From 5,000 JPY",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const extendedServices = [...services, ...services, ...services];
|
const isScrollingRef = useRef(false);
|
||||||
|
const autoScrollTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Compute item width for scrolling
|
||||||
|
const computeItemWidth = useCallback(() => {
|
||||||
|
const container = carouselRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const card = container.querySelector<HTMLElement>("[data-service-card]");
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
const gap =
|
||||||
|
Number.parseFloat(getComputedStyle(container).columnGap || "0") ||
|
||||||
|
Number.parseFloat(getComputedStyle(container).gap || "0") ||
|
||||||
|
24;
|
||||||
|
|
||||||
|
itemWidthRef.current = card.getBoundingClientRect().width + gap;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Scroll by one card width
|
||||||
|
const scrollByOne = useCallback((direction: 1 | -1) => {
|
||||||
|
const container = carouselRef.current;
|
||||||
|
if (!container || !itemWidthRef.current || isScrollingRef.current) return;
|
||||||
|
|
||||||
|
isScrollingRef.current = true;
|
||||||
|
|
||||||
|
container.scrollBy({
|
||||||
|
left: direction * itemWidthRef.current,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset scrolling flag after animation completes
|
||||||
|
setTimeout(() => {
|
||||||
|
isScrollingRef.current = false;
|
||||||
|
}, 500);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Start auto-scroll timer
|
||||||
|
const startAutoScroll = useCallback(() => {
|
||||||
|
if (autoScrollTimerRef.current) {
|
||||||
|
clearInterval(autoScrollTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
autoScrollTimerRef.current = setInterval(() => {
|
||||||
|
scrollByOne(1);
|
||||||
|
}, 5000);
|
||||||
|
}, [scrollByOne]);
|
||||||
|
|
||||||
|
// Stop auto-scroll timer
|
||||||
|
const stopAutoScroll = useCallback(() => {
|
||||||
|
if (autoScrollTimerRef.current) {
|
||||||
|
clearInterval(autoScrollTimerRef.current);
|
||||||
|
autoScrollTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const supportDownloads = [
|
const supportDownloads = [
|
||||||
{
|
{
|
||||||
@ -89,68 +214,43 @@ export function PublicLandingView() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const scrollServices = useCallback(
|
// Initialize carousel
|
||||||
(direction: 1 | -1) => {
|
|
||||||
const container = carouselRef.current;
|
|
||||||
if (!container) return;
|
|
||||||
const card = container.querySelector<HTMLElement>("[data-service-card]");
|
|
||||||
const gap =
|
|
||||||
Number.parseFloat(getComputedStyle(container).columnGap || "0") ||
|
|
||||||
Number.parseFloat(getComputedStyle(container).gap || "0") ||
|
|
||||||
24;
|
|
||||||
const amount = card ? card.clientWidth + gap : container.clientWidth;
|
|
||||||
|
|
||||||
const nearEnd = container.scrollLeft >= container.scrollWidth - container.clientWidth - 10;
|
|
||||||
const atStart = container.scrollLeft <= 0;
|
|
||||||
|
|
||||||
if (direction > 0 && nearEnd) {
|
|
||||||
container.scrollTo({ left: 0, behavior: "smooth" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction < 0 && atStart) {
|
|
||||||
container.scrollTo({
|
|
||||||
left: container.scrollWidth - container.clientWidth,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.scrollBy({ left: direction * amount, behavior: "smooth" });
|
|
||||||
},
|
|
||||||
[carouselRef]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = carouselRef.current;
|
computeItemWidth();
|
||||||
if (container) {
|
|
||||||
container.scrollLeft = container.scrollWidth / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
const interval = window.setInterval(() => {
|
const handleResize = () => computeItemWidth();
|
||||||
scrollServices(1);
|
window.addEventListener("resize", handleResize);
|
||||||
}, 10000);
|
|
||||||
return () => window.clearInterval(interval);
|
|
||||||
}, [scrollServices]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Start auto-scroll
|
||||||
const container = carouselRef.current;
|
startAutoScroll();
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
return () => {
|
||||||
const segmentWidth = container.scrollWidth / 3;
|
window.removeEventListener("resize", handleResize);
|
||||||
const threshold = segmentWidth * 0.05;
|
stopAutoScroll();
|
||||||
|
|
||||||
if (container.scrollLeft >= segmentWidth * 2 - threshold) {
|
|
||||||
container.scrollLeft -= segmentWidth;
|
|
||||||
} else if (container.scrollLeft <= threshold) {
|
|
||||||
container.scrollLeft += segmentWidth;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}, [computeItemWidth, startAutoScroll, stopAutoScroll]);
|
||||||
|
|
||||||
container.addEventListener("scroll", handleScroll, { passive: true });
|
// Pause auto-scroll on hover
|
||||||
return () => container.removeEventListener("scroll", handleScroll);
|
const handleMouseEnter = useCallback(() => {
|
||||||
}, []);
|
stopAutoScroll();
|
||||||
|
}, [stopAutoScroll]);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
startAutoScroll();
|
||||||
|
}, [startAutoScroll]);
|
||||||
|
|
||||||
|
// Manual scroll handlers
|
||||||
|
const handlePrevClick = useCallback(() => {
|
||||||
|
scrollByOne(-1);
|
||||||
|
// Restart auto-scroll timer
|
||||||
|
startAutoScroll();
|
||||||
|
}, [scrollByOne, startAutoScroll]);
|
||||||
|
|
||||||
|
const handleNextClick = useCallback(() => {
|
||||||
|
scrollByOne(1);
|
||||||
|
// Restart auto-scroll timer
|
||||||
|
startAutoScroll();
|
||||||
|
}, [scrollByOne, startAutoScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0 pb-8 pt-0">
|
<div className="space-y-0 pb-8 pt-0">
|
||||||
@ -184,65 +284,52 @@ export function PublicLandingView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-2xl border border-border/70 bg-white shadow-lg">
|
<div className="rounded-2xl border border-border/70 bg-white shadow-lg p-6">
|
||||||
<Image
|
{/* Tab Switcher */}
|
||||||
src="/assets/images/Hero Image.png"
|
<div className="flex mb-6 bg-gray-100 rounded-full p-1">
|
||||||
alt="Team collaborating in a modern office"
|
<button
|
||||||
fill
|
type="button"
|
||||||
className="object-cover"
|
onClick={() => setActiveCategory("personal")}
|
||||||
priority
|
className={`flex-1 py-2.5 px-4 text-sm font-semibold rounded-full transition-all ${
|
||||||
/>
|
activeCategory === "personal"
|
||||||
</div>
|
? "bg-gray-900 text-white shadow-md"
|
||||||
</div>
|
: "text-gray-600 hover:text-gray-900"
|
||||||
</div>
|
}`}
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Solutions Carousel */}
|
|
||||||
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#e8f5ff] py-12 sm:py-14">
|
|
||||||
<div className="mx-auto max-w-7xl px-6 sm:px-10 lg:px-14">
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<h2 className="text-3xl sm:text-4xl font-extrabold text-foreground">Solutions</h2>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
ref={carouselRef}
|
|
||||||
className="flex gap-6 overflow-x-auto scroll-smooth pb-6 pr-16"
|
|
||||||
style={{ scrollbarWidth: "none" }}
|
|
||||||
>
|
|
||||||
{extendedServices.map((service, index) => (
|
|
||||||
<Link
|
|
||||||
key={`${service.title}-${index}`}
|
|
||||||
href={service.href}
|
|
||||||
className="group flex-shrink-0 w-[260px]"
|
|
||||||
>
|
>
|
||||||
<article
|
Personal Services
|
||||||
data-service-card
|
</button>
|
||||||
className="h-full rounded-3xl bg-white px-6 py-8 shadow-md border border-white/60 transition-transform duration-300 group-hover:-translate-y-1"
|
<button
|
||||||
>
|
type="button"
|
||||||
<div className="mb-5">{service.icon}</div>
|
onClick={() => setActiveCategory("business")}
|
||||||
<h3 className="text-xl font-semibold text-foreground mb-3">{service.title}</h3>
|
className={`flex-1 py-2.5 px-4 text-sm font-semibold rounded-full transition-all ${
|
||||||
<p className="text-muted-foreground leading-relaxed">{service.description}</p>
|
activeCategory === "business"
|
||||||
</article>
|
? "bg-gray-900 text-white shadow-md"
|
||||||
</Link>
|
: "text-gray-600 hover:text-gray-900"
|
||||||
))}
|
}`}
|
||||||
</div>
|
>
|
||||||
<div className="absolute bottom-0 right-0 flex gap-3 pr-2">
|
Business Services
|
||||||
<button
|
</button>
|
||||||
type="button"
|
</div>
|
||||||
aria-label="Scroll solutions left"
|
|
||||||
onClick={() => scrollServices(-1)}
|
{/* Services Grid */}
|
||||||
className="h-10 w-10 rounded-full border border-black/20 bg-white/80 text-foreground shadow-sm hover:bg-white transition-colors"
|
<div className="grid grid-cols-2 gap-4">
|
||||||
>
|
{(activeCategory === "personal" ? personalServices : businessServices).map(
|
||||||
<span aria-hidden>‹</span>
|
service => (
|
||||||
</button>
|
<Link
|
||||||
<button
|
key={service.title}
|
||||||
type="button"
|
href={service.href}
|
||||||
aria-label="Scroll solutions right"
|
className="group flex flex-col items-center gap-3 p-4 rounded-xl hover:bg-gray-50 transition-colors"
|
||||||
onClick={() => scrollServices(1)}
|
>
|
||||||
className="h-10 w-10 rounded-full border border-black/20 bg-white/80 text-foreground shadow-sm hover:bg-white transition-colors"
|
<div className="w-16 h-16 rounded-full border-2 border-primary/30 flex items-center justify-center group-hover:border-primary/60 transition-colors">
|
||||||
>
|
{service.icon}
|
||||||
<span aria-hidden>›</span>
|
</div>
|
||||||
</button>
|
<span className="text-sm font-semibold text-foreground text-center">
|
||||||
|
{service.title}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -270,6 +357,39 @@ export function PublicLandingView() {
|
|||||||
technological challenges with confidence.
|
technological challenges with confidence.
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Trust Badges */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center gap-3 p-3 rounded-lg bg-primary/5">
|
||||||
|
<Calendar className="h-5 w-5 text-primary flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-foreground">Est. 2002</p>
|
||||||
|
<p className="text-xs text-muted-foreground">20+ Years Experience</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 p-3 rounded-lg bg-primary/5">
|
||||||
|
<ShieldCheck className="h-5 w-5 text-primary flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-foreground">Registered Carrier</p>
|
||||||
|
<p className="text-xs text-muted-foreground">A-19-9538</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 p-3 rounded-lg bg-primary/5">
|
||||||
|
<Languages className="h-5 w-5 text-primary flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-foreground">Bilingual Support</p>
|
||||||
|
<p className="text-xs text-muted-foreground">English & Japanese</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 p-3 rounded-lg bg-primary/5">
|
||||||
|
<Users className="h-5 w-5 text-primary flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-foreground">21 Staff Members</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Dedicated Team</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul className="space-y-3 text-foreground">
|
<ul className="space-y-3 text-foreground">
|
||||||
{["One Stop Solution", "English Support", "Onsite Support"].map(item => (
|
{["One Stop Solution", "English Support", "Onsite Support"].map(item => (
|
||||||
<li key={item} className="flex items-center gap-3 text-base font-semibold">
|
<li key={item} className="flex items-center gap-3 text-base font-semibold">
|
||||||
@ -289,6 +409,60 @@ export function PublicLandingView() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Solutions Carousel */}
|
||||||
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#e8f5ff] py-12 sm:py-14">
|
||||||
|
<div className="mx-auto max-w-7xl px-6 sm:px-10 lg:px-14">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-3xl sm:text-4xl font-extrabold text-foreground">Solutions</h2>
|
||||||
|
</div>
|
||||||
|
<div className="relative" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||||
|
<div
|
||||||
|
ref={carouselRef}
|
||||||
|
className="flex gap-6 overflow-x-auto scroll-smooth pb-6 pr-16 snap-x snap-mandatory"
|
||||||
|
style={{ scrollbarWidth: "none", WebkitOverflowScrolling: "touch" }}
|
||||||
|
>
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<Link
|
||||||
|
key={`${service.title}-${index}`}
|
||||||
|
href={service.href}
|
||||||
|
className="group flex-shrink-0 w-[260px] snap-start"
|
||||||
|
>
|
||||||
|
<article
|
||||||
|
data-service-card
|
||||||
|
className="h-full rounded-3xl bg-white px-6 py-8 shadow-md border border-white/60 transition-transform duration-300 group-hover:-translate-y-1 flex flex-col"
|
||||||
|
>
|
||||||
|
<div className="mb-5">{service.icon}</div>
|
||||||
|
<h3 className="text-xl font-semibold text-foreground mb-3">{service.title}</h3>
|
||||||
|
<p className="text-muted-foreground leading-relaxed flex-grow">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-bold text-primary mt-4">{service.price}</p>
|
||||||
|
</article>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 right-0 flex gap-3 pr-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Scroll solutions left"
|
||||||
|
onClick={handlePrevClick}
|
||||||
|
className="h-10 w-10 rounded-full border border-black/20 bg-white/80 text-foreground shadow-sm hover:bg-white transition-colors active:scale-95"
|
||||||
|
>
|
||||||
|
<span aria-hidden>‹</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Scroll solutions right"
|
||||||
|
onClick={handleNextClick}
|
||||||
|
className="h-10 w-10 rounded-full border border-black/20 bg-white/80 text-foreground shadow-sm hover:bg-white transition-colors active:scale-95"
|
||||||
|
>
|
||||||
|
<span aria-hidden>›</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Support Downloads */}
|
{/* Support Downloads */}
|
||||||
<section className="max-w-5xl mx-auto px-6 sm:px-10 lg:px-14 pb-16">
|
<section className="max-w-5xl mx-auto px-6 sm:px-10 lg:px-14 pb-16">
|
||||||
<h3 className="text-center text-3xl sm:text-4xl font-extrabold text-primary tracking-tight mb-10">
|
<h3 className="text-center text-3xl sm:text-4xl font-extrabold text-primary tracking-tight mb-10">
|
||||||
@ -298,7 +472,7 @@ export function PublicLandingView() {
|
|||||||
{supportDownloads.map(item => (
|
{supportDownloads.map(item => (
|
||||||
<div
|
<div
|
||||||
key={item.title}
|
key={item.title}
|
||||||
className="flex flex-col items-center gap-4 rounded-2xl bg-white p-6 shadow-md"
|
className="flex flex-col items-center gap-4 rounded-2xl bg-white p-6"
|
||||||
>
|
>
|
||||||
<div className="text-center space-y-2">
|
<div className="text-center space-y-2">
|
||||||
<p className="text-lg font-semibold text-foreground leading-snug">{item.title}</p>
|
<p className="text-lg font-semibold text-foreground leading-snug">{item.title}</p>
|
||||||
@ -398,7 +572,7 @@ export function PublicLandingView() {
|
|||||||
<div className="w-full rounded-2xl overflow-hidden shadow-md border border-border/60 bg-white aspect-[4/3]">
|
<div className="w-full rounded-2xl overflow-hidden shadow-md border border-border/60 bg-white aspect-[4/3]">
|
||||||
<iframe
|
<iframe
|
||||||
title="Assist Solutions Corp Map"
|
title="Assist Solutions Corp Map"
|
||||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3241.549653770843!2d139.7386725760842!3d35.66382513061992!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x60188bce85cdcfdf%3A0x1c65fcd01aaa6d2f!2s3-ch%C5%8Dme-8-2%20Higashiazabu%2C%20Minato%20City%2C%20Tokyo%20106-0044%2C%20Japan!5e0!3m2!1sen!2sus!4v1700000000000!5m2!1sen!2sus"
|
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"
|
className="w-full h-full"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
Wifi,
|
||||||
|
Smartphone,
|
||||||
Building2,
|
Building2,
|
||||||
Users,
|
Tv,
|
||||||
Calendar,
|
Lock,
|
||||||
CircleDollarSign,
|
Wrench,
|
||||||
Phone,
|
Heart,
|
||||||
MapPin,
|
Clock3,
|
||||||
Clock,
|
Lightbulb,
|
||||||
CheckCircle,
|
Globe,
|
||||||
|
Shield,
|
||||||
|
Quote,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,185 +24,315 @@ import {
|
|||||||
* and mission statement for Assist Solutions.
|
* and mission statement for Assist Solutions.
|
||||||
*/
|
*/
|
||||||
export function AboutUsView() {
|
export function AboutUsView() {
|
||||||
return (
|
const values = [
|
||||||
<div className="max-w-4xl mx-auto space-y-12">
|
{
|
||||||
{/* Header */}
|
text: "Provide the most customer-oriented service in this industry in Japan.",
|
||||||
<div className="text-center">
|
icon: <Heart className="h-6 w-6" />,
|
||||||
<h1 className="text-3xl sm:text-4xl font-bold text-foreground mb-4">About Us</h1>
|
color: "bg-rose-50 text-rose-500 border-rose-100",
|
||||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
},
|
||||||
We specialize in serving Japan's international community with the most reliable and
|
{
|
||||||
cost-efficient IT solutions available.
|
text: "Through our service, we save client's time and enrich customers' lives.",
|
||||||
</p>
|
icon: <Clock3 className="h-6 w-6" />,
|
||||||
</div>
|
color: "bg-amber-50 text-amber-500 border-amber-100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "We always have the latest and most efficient knowledge required for our service.",
|
||||||
|
icon: <Lightbulb className="h-6 w-6" />,
|
||||||
|
color: "bg-sky-50 text-sky-500 border-sky-100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Be a responsible participant in Japan's international community.",
|
||||||
|
icon: <Globe className="h-6 w-6" />,
|
||||||
|
color: "bg-emerald-50 text-emerald-500 border-emerald-100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Maintain high ethical standards in all business activities.",
|
||||||
|
icon: <Shield className="h-6 w-6" />,
|
||||||
|
color: "bg-violet-50 text-violet-500 border-violet-100",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
{/* Who We Are Section */}
|
const services = [
|
||||||
<section className="bg-card rounded-2xl border border-border p-8 sm:p-10">
|
{
|
||||||
<div className="flex items-center gap-3 mb-6">
|
title: "Internet Plans",
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center">
|
description:
|
||||||
<Building2 className="h-6 w-6 text-primary" />
|
"Utilizing NTT's optical fiber network, we deliver one of the most reliable Internet connections in Japan.",
|
||||||
|
icon: <Wifi className="h-7 w-7 text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Phone Plans",
|
||||||
|
description:
|
||||||
|
"Using NTT DOCOMO's network, we deliver cost-friendly SIM card services across Japan.",
|
||||||
|
icon: <Smartphone className="h-7 w-7 text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Business Solutions",
|
||||||
|
description: "Dedicated Internet access, office network setup, data center services.",
|
||||||
|
icon: <Building2 className="h-7 w-7 text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "VPN",
|
||||||
|
description: "Connect directly from Tokyo with static routing across global endpoints.",
|
||||||
|
icon: <Lock className="h-7 w-7 text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "TV Services",
|
||||||
|
description: "Options for Satellite TV and Optical Fiber TV tailored to your needs.",
|
||||||
|
icon: <Tv className="h-7 w-7 text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Onsite Support",
|
||||||
|
description: "Professional setup, configuration, and troubleshooting at your location.",
|
||||||
|
icon: <Wrench className="h-7 w-7 text-primary" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const carouselRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [scrollAmount, setScrollAmount] = useState(0);
|
||||||
|
|
||||||
|
const computeScrollAmount = useCallback(() => {
|
||||||
|
const container = carouselRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const card = container.querySelector<HTMLElement>("[data-business-card]");
|
||||||
|
if (!card) return;
|
||||||
|
const gap =
|
||||||
|
Number.parseFloat(getComputedStyle(container).columnGap || "0") ||
|
||||||
|
Number.parseFloat(getComputedStyle(container).gap || "0") ||
|
||||||
|
24;
|
||||||
|
setScrollAmount(card.clientWidth + gap);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollServices = useCallback(
|
||||||
|
(direction: 1 | -1) => {
|
||||||
|
const container = carouselRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const amount = scrollAmount || container.clientWidth;
|
||||||
|
container.scrollBy({ left: direction * amount, behavior: "smooth" });
|
||||||
|
},
|
||||||
|
[scrollAmount]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
computeScrollAmount();
|
||||||
|
window.addEventListener("resize", computeScrollAmount);
|
||||||
|
return () => window.removeEventListener("resize", computeScrollAmount);
|
||||||
|
}, [computeScrollAmount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-6xl mx-auto space-y-12 px-6 sm:px-8">
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="grid grid-cols-1 lg:grid-cols-[1.05fr_minmax(0,0.95fr)] gap-10 items-center">
|
||||||
|
<div className="space-y-5">
|
||||||
|
<h1 className="text-3xl sm:text-4xl font-bold text-primary leading-tight">About Us</h1>
|
||||||
|
<div className="space-y-4 text-muted-foreground leading-relaxed">
|
||||||
|
<p>
|
||||||
|
Assist Solutions Corp. is a privately-owned entrepreneurial IT service company. We
|
||||||
|
specialize in serving Japan's international community with the most reliable and
|
||||||
|
cost-efficient IT & TV solutions available.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We are dedicated to providing comfortable support for our customer's diverse
|
||||||
|
needs in both English and Japanese. Our excellent bi-lingual support, flexible
|
||||||
|
service, and deep industry knowledge set us apart.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-foreground">Who We Are</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4 text-muted-foreground leading-relaxed">
|
<div className="relative h-full min-h-[420px]">
|
||||||
<p>
|
<Image
|
||||||
Assist Solutions Corp. is a privately-owned entrepreneurial IT service company. We
|
src="/assets/images/About us.png"
|
||||||
specialize in serving Japan's international community with the most reliable and
|
alt="Assist Solutions team in Tokyo"
|
||||||
cost-efficient IT & TV solutions available.
|
fill
|
||||||
</p>
|
priority
|
||||||
<p>
|
className="object-contain"
|
||||||
We are dedicated to providing comfortable support for our customer's diverse needs
|
sizes="(max-width: 1024px) 100vw, 45vw"
|
||||||
in both English and Japanese. We believe that our excellent bi-lingual support and
|
/>
|
||||||
flexible service along with our knowledge and experience in the field are what sets us
|
</div>
|
||||||
apart from the rest of the information technology and broadcasting industry.
|
</section>
|
||||||
|
|
||||||
|
{/* Business Solutions Carousel */}
|
||||||
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#e8f5ff] py-10">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 sm:px-10">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-foreground">Business</h2>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
ref={carouselRef}
|
||||||
|
className="flex gap-6 overflow-x-auto scroll-smooth pb-6 pr-20"
|
||||||
|
style={{ scrollbarWidth: "none" }}
|
||||||
|
>
|
||||||
|
{services.map((service, idx) => (
|
||||||
|
<article
|
||||||
|
key={`${service.title}-${idx}`}
|
||||||
|
data-business-card
|
||||||
|
className="flex-shrink-0 w-[240px] rounded-3xl bg-white px-6 py-7 shadow-md border border-white/60"
|
||||||
|
>
|
||||||
|
<div className="mb-4">{service.icon}</div>
|
||||||
|
<h3 className="text-lg font-semibold text-foreground mb-2">{service.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 right-2 flex gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => scrollServices(-1)}
|
||||||
|
className="h-9 w-9 rounded-full border border-black/15 bg-white text-foreground shadow-sm hover:bg-white/90"
|
||||||
|
aria-label="Scroll business left"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => scrollServices(1)}
|
||||||
|
className="h-9 w-9 rounded-full border border-black/15 bg-white text-foreground shadow-sm hover:bg-white/90"
|
||||||
|
aria-label="Scroll business right"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Our Values Section */}
|
||||||
|
<section className="space-y-8">
|
||||||
|
<div className="text-center max-w-2xl mx-auto">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-primary mb-3">Our Values</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
These principles guide how we serve customers, support our community, and advance our
|
||||||
|
craft every day.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Values Grid - 3 on top, 2 centered on bottom */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Top row - 3 cards */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{values.slice(0, 3).map((value, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="group relative bg-white rounded-2xl border border-border/60 p-6 shadow-sm hover:shadow-md hover:-translate-y-1 transition-all duration-300"
|
||||||
|
>
|
||||||
|
{/* Icon */}
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center justify-center w-12 h-12 rounded-xl border mb-4 ${value.color}`}
|
||||||
|
>
|
||||||
|
{value.icon}
|
||||||
|
</div>
|
||||||
|
{/* Quote mark decoration */}
|
||||||
|
<Quote className="absolute top-4 right-4 h-8 w-8 text-muted-foreground/10 rotate-180" />
|
||||||
|
{/* Text */}
|
||||||
|
<p className="text-foreground font-medium leading-relaxed">{value.text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom row - 2 cards centered */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-2xl mx-auto lg:max-w-none lg:grid-cols-2 lg:px-[16.666%]">
|
||||||
|
{values.slice(3).map((value, index) => (
|
||||||
|
<div
|
||||||
|
key={index + 3}
|
||||||
|
className="group relative bg-white rounded-2xl border border-border/60 p-6 shadow-sm hover:shadow-md hover:-translate-y-1 transition-all duration-300"
|
||||||
|
>
|
||||||
|
{/* Icon */}
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center justify-center w-12 h-12 rounded-xl border mb-4 ${value.color}`}
|
||||||
|
>
|
||||||
|
{value.icon}
|
||||||
|
</div>
|
||||||
|
{/* Quote mark decoration */}
|
||||||
|
<Quote className="absolute top-4 right-4 h-8 w-8 text-muted-foreground/10 rotate-180" />
|
||||||
|
{/* Text */}
|
||||||
|
<p className="text-foreground font-medium leading-relaxed">{value.text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Corporate Data Section */}
|
{/* Corporate Data Section */}
|
||||||
<section className="bg-card rounded-2xl border border-border p-8 sm:p-10">
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-12">
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="max-w-6xl mx-auto px-6 sm:px-8 space-y-6">
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center">
|
<div className="flex items-center gap-3">
|
||||||
<Users className="h-6 w-6 text-primary" />
|
<h2 className="text-2xl font-bold text-foreground">Corporate Data</h2>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-foreground">Corporate Data</h2>
|
<div className="grid grid-cols-1 lg:grid-cols-[1.2fr_0.8fr] gap-10 items-start">
|
||||||
</div>
|
<div className="space-y-5">
|
||||||
<p className="text-muted-foreground mb-6">
|
<div>
|
||||||
Assist Solutions is a privately-owned entrepreneurial IT supporting company, focused on
|
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
|
||||||
the international community in Japan.
|
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||||
</p>
|
Representative Director
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground font-semibold mt-1">Daisuke Nagakawa</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-border">
|
<div>
|
||||||
{/* Company Name */}
|
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
|
||||||
<div className="py-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||||
<div className="font-medium text-foreground">Name</div>
|
Employees
|
||||||
<div className="sm:col-span-2 text-muted-foreground">
|
</h3>
|
||||||
Assist Solutions Corp.
|
<p className="text-muted-foreground font-semibold mt-1">
|
||||||
<br />
|
21 Staff Members (as of March 31st, 2025)
|
||||||
<span className="text-sm">(Notified Telecommunication Carrier: A-19-9538)</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Address */}
|
<div>
|
||||||
<div className="py-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||||
<MapPin className="h-4 w-4 text-primary" />
|
Established
|
||||||
Address
|
</h3>
|
||||||
</div>
|
<p className="text-muted-foreground font-semibold mt-1">March 8, 2002</p>
|
||||||
<div className="sm:col-span-2 text-muted-foreground">
|
</div>
|
||||||
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu,
|
|
||||||
<br />
|
|
||||||
Minato-ku, Tokyo 106-0044
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Phone/Fax */}
|
<div>
|
||||||
<div className="py-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||||
<Phone className="h-4 w-4 text-primary" />
|
Paid in Capital
|
||||||
Tel / Fax
|
</h3>
|
||||||
</div>
|
<p className="text-muted-foreground font-semibold mt-1">40,000,000 JPY</p>
|
||||||
<div className="sm:col-span-2 text-muted-foreground">
|
</div>
|
||||||
Tel: 03-3560-1006
|
|
||||||
<br />
|
|
||||||
Fax: 03-3560-1007
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Business Hours */}
|
<div>
|
||||||
<div className="py-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<h3 className="text-xl font-semibold text-foreground flex items-center gap-2">
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
<span className="h-6 w-1 rounded-full bg-primary" />
|
||||||
<Clock className="h-4 w-4 text-primary" />
|
Business Hours
|
||||||
Business Hours
|
</h3>
|
||||||
|
<div className="text-muted-foreground font-semibold mt-1 space-y-1">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2 text-muted-foreground space-y-1">
|
|
||||||
<div>Mon - Fri 9:30AM - 6:00PM — Customer Support Team</div>
|
|
||||||
<div>Mon - Fri 9:30AM - 6:00PM — In-office Tech Support Team</div>
|
|
||||||
<div>Mon - Sat 10:00AM - 9:00PM — Onsite Tech Support Team</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Representative */}
|
<div className="space-y-4">
|
||||||
<div className="py-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<div className="bg-muted/20 rounded-2xl border border-border/60 p-4">
|
||||||
<div className="font-medium text-foreground">Representative Director</div>
|
<h3 className="text-xl font-bold text-foreground mb-2 text-center">Address</h3>
|
||||||
<div className="sm:col-span-2 text-muted-foreground">Daisuke Nagakawa</div>
|
<p className="text-center text-muted-foreground font-semibold leading-relaxed">
|
||||||
</div>
|
3F Azabu Maruka Bldg., 3-8-2 Higashi Azabu,
|
||||||
|
<br />
|
||||||
{/* Employees */}
|
Minato-ku, Tokyo 106-0044
|
||||||
<div className="py-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<br />
|
||||||
<div className="font-medium text-foreground">Employees</div>
|
Tel: 03-3560-1006 Fax: 03-3560-1007
|
||||||
<div className="sm:col-span-2 text-muted-foreground">
|
</p>
|
||||||
21 Staff Members (as of March 31st, 2025)
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Established */}
|
|
||||||
<div className="py-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
|
||||||
<Calendar className="h-4 w-4 text-primary" />
|
|
||||||
Established
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2 text-muted-foreground">March 8, 2002</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Capital */}
|
|
||||||
<div className="py-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
||||||
<div className="font-medium text-foreground flex items-center gap-2">
|
|
||||||
<CircleDollarSign className="h-4 w-4 text-primary" />
|
|
||||||
Paid-in Capital
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2 text-muted-foreground">40,000,000 JPY</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Business Activities Section */}
|
|
||||||
<section className="bg-card rounded-2xl border border-border p-8 sm:p-10">
|
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-6">Business Activities</h2>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
{[
|
|
||||||
"IT Consulting Services",
|
|
||||||
"TV Consulting Services",
|
|
||||||
"Internet Connection Service Provision (SonixNet ISP)",
|
|
||||||
"VPN Connection Service Provision (SonixNet US/UK Remote Access)",
|
|
||||||
"Agent for Telecommunication Services",
|
|
||||||
"Agent for Internet Services",
|
|
||||||
"Agent for TV Services",
|
|
||||||
"Onsite Support Service for IT",
|
|
||||||
"Onsite Support Service for TV",
|
|
||||||
"Server Management Service",
|
|
||||||
"Network Management Service",
|
|
||||||
].map((activity, index) => (
|
|
||||||
<div key={index} className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" />
|
|
||||||
<span className="text-muted-foreground">{activity}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Mission Statement Section */}
|
|
||||||
<section className="bg-gradient-to-br from-primary/5 to-transparent rounded-2xl border border-primary/20 p-8 sm:p-10">
|
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-6">Mission Statement</h2>
|
|
||||||
<p className="text-muted-foreground mb-6 leading-relaxed">
|
|
||||||
We will achieve business success by pursuing the following:
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-4">
|
|
||||||
{[
|
|
||||||
"Provide the most customer-oriented service in this industry in Japan.",
|
|
||||||
"Through our service, we save client's time and enrich customers' lives.",
|
|
||||||
"We always have the latest and most efficient knowledge required for our service.",
|
|
||||||
"Be a responsible participant in Japan's international community.",
|
|
||||||
"Maintain high ethical standards in all business activities.",
|
|
||||||
].map((mission, index) => (
|
|
||||||
<li key={index} className="flex items-start gap-3">
|
|
||||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-primary text-sm font-semibold flex-shrink-0">
|
|
||||||
{index + 1}
|
|
||||||
</span>
|
|
||||||
<span className="text-foreground leading-relaxed">{mission}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import { cn } from "@/shared/utils";
|
|||||||
interface TierInfo {
|
interface TierInfo {
|
||||||
tier: "Silver" | "Gold" | "Platinum";
|
tier: "Silver" | "Gold" | "Platinum";
|
||||||
monthlyPrice: number;
|
monthlyPrice: number;
|
||||||
|
/** Max price for showing price range (when prices vary by offering type) */
|
||||||
|
maxMonthlyPrice?: number;
|
||||||
description: string;
|
description: string;
|
||||||
features: string[];
|
features: string[];
|
||||||
pricingNote?: string;
|
pricingNote?: string;
|
||||||
@ -21,6 +23,8 @@ interface PublicOfferingCardProps {
|
|||||||
description: string;
|
description: string;
|
||||||
iconType: "home" | "apartment";
|
iconType: "home" | "apartment";
|
||||||
startingPrice: number;
|
startingPrice: number;
|
||||||
|
/** Maximum price for showing price range */
|
||||||
|
maxPrice?: number;
|
||||||
setupFee: number;
|
setupFee: number;
|
||||||
tiers: TierInfo[];
|
tiers: TierInfo[];
|
||||||
isPremium?: boolean;
|
isPremium?: boolean;
|
||||||
@ -112,6 +116,7 @@ export function PublicOfferingCard({
|
|||||||
description,
|
description,
|
||||||
iconType,
|
iconType,
|
||||||
startingPrice,
|
startingPrice,
|
||||||
|
maxPrice,
|
||||||
setupFee,
|
setupFee,
|
||||||
tiers,
|
tiers,
|
||||||
isPremium = false,
|
isPremium = false,
|
||||||
@ -163,6 +168,7 @@ export function PublicOfferingCard({
|
|||||||
<span className="text-xs text-muted-foreground">From</span>
|
<span className="text-xs text-muted-foreground">From</span>
|
||||||
<span className="text-lg font-bold text-foreground">
|
<span className="text-lg font-bold text-foreground">
|
||||||
¥{startingPrice.toLocaleString()}
|
¥{startingPrice.toLocaleString()}
|
||||||
|
{maxPrice && maxPrice > startingPrice && `~${maxPrice.toLocaleString()}`}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">/mo</span>
|
<span className="text-sm text-muted-foreground">/mo</span>
|
||||||
</div>
|
</div>
|
||||||
@ -223,6 +229,9 @@ export function PublicOfferingCard({
|
|||||||
<div className="flex items-baseline gap-0.5 flex-wrap">
|
<div className="flex items-baseline gap-0.5 flex-wrap">
|
||||||
<span className="text-xl font-bold text-foreground">
|
<span className="text-xl font-bold text-foreground">
|
||||||
¥{tier.monthlyPrice.toLocaleString()}
|
¥{tier.monthlyPrice.toLocaleString()}
|
||||||
|
{tier.maxMonthlyPrice &&
|
||||||
|
tier.maxMonthlyPrice > tier.monthlyPrice &&
|
||||||
|
`~${tier.maxMonthlyPrice.toLocaleString()}`}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">/mo</span>
|
<span className="text-xs text-muted-foreground">/mo</span>
|
||||||
{tier.pricingNote && (
|
{tier.pricingNote && (
|
||||||
|
|||||||
@ -607,6 +607,35 @@ export function SimPlansContent({
|
|||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
<div className="mt-12 mb-8">
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 max-w-3xl mx-auto">
|
||||||
|
<FaqItem
|
||||||
|
question="Can I keep my existing phone number when switching to your SIM?"
|
||||||
|
answer="Yes, we support Mobile Number Portability (MNP). You'll need to obtain an MNP reservation number from your current carrier and provide it during signup. The transfer typically completes within a few days."
|
||||||
|
/>
|
||||||
|
<FaqItem
|
||||||
|
question="Do I need a Japanese bank account or credit history?"
|
||||||
|
answer="No Japanese bank account or credit history is required. We accept international credit cards (Visa, Mastercard, American Express) for payment. ID verification is done through document submission."
|
||||||
|
/>
|
||||||
|
<FaqItem
|
||||||
|
question="Can I use my SIM card overseas?"
|
||||||
|
answer="International data roaming is not available. However, voice calls and SMS can be enabled for international roaming upon request, with a monthly limit of ¥50,000 to prevent bill shock."
|
||||||
|
/>
|
||||||
|
<FaqItem
|
||||||
|
question="How do I check my remaining data balance?"
|
||||||
|
answer="You can check your data usage anytime through our customer portal. We also send notifications when you reach 80% and 100% of your data allowance."
|
||||||
|
/>
|
||||||
|
<FaqItem
|
||||||
|
question="What happens if I exceed my data limit?"
|
||||||
|
answer="When you reach your data limit, speeds are throttled to 200kbps for the rest of the month. You can purchase additional data top-ups through your account portal to restore full speed."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 text-center text-sm text-muted-foreground">
|
<div className="mt-8 text-center text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
All prices exclude 10% consumption tax.{" "}
|
All prices exclude 10% consumption tax.{" "}
|
||||||
@ -619,4 +648,26 @@ export function SimPlansContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-border rounded-xl overflow-hidden bg-card">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="w-full flex items-start justify-between gap-3 p-4 text-left hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="font-medium text-foreground">{question}</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="px-4 pb-4 text-sm text-muted-foreground leading-relaxed">{answer}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default SimPlansContent;
|
export default SimPlansContent;
|
||||||
|
|||||||
@ -15,33 +15,163 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { usePublicInternetCatalog } from "@/features/services/hooks";
|
import { usePublicInternetCatalog } from "@/features/services/hooks";
|
||||||
import type {
|
import type {
|
||||||
InternetPlanCatalogItem,
|
|
||||||
InternetInstallationCatalogItem,
|
InternetInstallationCatalogItem,
|
||||||
|
InternetPlanCatalogItem,
|
||||||
} from "@customer-portal/domain/services";
|
} from "@customer-portal/domain/services";
|
||||||
import { Skeleton } from "@/components/atoms/loading-skeleton";
|
import { Skeleton } from "@/components/atoms/loading-skeleton";
|
||||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||||
import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink";
|
import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink";
|
||||||
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
||||||
import { Button } from "@/components/atoms/button";
|
import { Button } from "@/components/atoms/button";
|
||||||
import { PublicOfferingCard } from "@/features/services/components/internet/PublicOfferingCard";
|
|
||||||
import type { TierInfo } from "@/features/services/components/internet/PublicOfferingCard";
|
import type { TierInfo } from "@/features/services/components/internet/PublicOfferingCard";
|
||||||
import {
|
import {
|
||||||
ServiceHighlights,
|
ServiceHighlights,
|
||||||
HighlightFeature,
|
HighlightFeature,
|
||||||
} from "@/features/services/components/base/ServiceHighlights";
|
} from "@/features/services/components/base/ServiceHighlights";
|
||||||
|
import { cn } from "@/shared/utils";
|
||||||
|
|
||||||
// Types
|
// Tier styling
|
||||||
interface GroupedOffering {
|
const tierStyles = {
|
||||||
offeringType: string;
|
Silver: {
|
||||||
title: string;
|
card: "border-muted-foreground/20 bg-card",
|
||||||
speedBadge: string;
|
accent: "text-muted-foreground",
|
||||||
description: string;
|
header: "Silver",
|
||||||
iconType: "home" | "apartment";
|
},
|
||||||
startingPrice: number;
|
Gold: {
|
||||||
|
card: "border-warning/30 bg-warning-soft/20",
|
||||||
|
accent: "text-warning",
|
||||||
|
header: "Gold",
|
||||||
|
},
|
||||||
|
Platinum: {
|
||||||
|
card: "border-primary/30 bg-info-soft/20",
|
||||||
|
accent: "text-primary",
|
||||||
|
header: "Platinum",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consolidated Internet Card - Single card showing all tiers with price ranges
|
||||||
|
*/
|
||||||
|
function ConsolidatedInternetCard({
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
setupFee,
|
||||||
|
tiers,
|
||||||
|
ctaPath,
|
||||||
|
ctaLabel,
|
||||||
|
onCtaClick,
|
||||||
|
}: {
|
||||||
|
minPrice: number;
|
||||||
|
maxPrice: number;
|
||||||
setupFee: number;
|
setupFee: number;
|
||||||
tiers: TierInfo[];
|
tiers: TierInfo[];
|
||||||
isPremium?: boolean;
|
ctaPath: string;
|
||||||
showConnectionInfo?: boolean;
|
ctaLabel: string;
|
||||||
|
onCtaClick?: (e: React.MouseEvent) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-border bg-card shadow-[var(--cp-shadow-1)] overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-5 border-b border-border bg-muted/20">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||||
|
<Wifi className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-foreground">NTT Fiber Internet</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Home & Apartment plans available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-left sm:text-right">
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<span className="text-2xl font-bold text-foreground">
|
||||||
|
¥{minPrice.toLocaleString()}~{maxPrice.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">/mo</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Price varies by location & tier</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tier Cards */}
|
||||||
|
<div className="p-5">
|
||||||
|
<p className="text-sm font-medium text-foreground mb-2">Choose your service tier:</p>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
Price varies based on your connection type: Home 10Gbps (select areas), Home 1Gbps, or
|
||||||
|
Apartment (up to 1Gbps depending on building infrastructure). We'll confirm
|
||||||
|
availability at your address.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||||
|
{tiers.map(tier => (
|
||||||
|
<div
|
||||||
|
key={tier.tier}
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl border p-4 transition-all duration-200 flex flex-col",
|
||||||
|
tierStyles[tier.tier].card
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Tier Name */}
|
||||||
|
<h4 className={cn("font-bold text-lg mb-2", tierStyles[tier.tier].accent)}>
|
||||||
|
{tier.tier}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Price Range */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex items-baseline gap-0.5 flex-wrap">
|
||||||
|
<span className="text-xl font-bold text-foreground">
|
||||||
|
¥{tier.monthlyPrice.toLocaleString()}
|
||||||
|
{tier.maxMonthlyPrice &&
|
||||||
|
tier.maxMonthlyPrice > tier.monthlyPrice &&
|
||||||
|
`~${tier.maxMonthlyPrice.toLocaleString()}`}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">/mo</span>
|
||||||
|
</div>
|
||||||
|
{tier.pricingNote && (
|
||||||
|
<span className="text-xs text-warning">{tier.pricingNote}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">{tier.description}</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<ul className="space-y-2 flex-grow">
|
||||||
|
{tier.features.map((feature, index) => (
|
||||||
|
<li key={index} className="flex items-start gap-2 text-sm">
|
||||||
|
<Zap className="h-4 w-4 text-success flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-muted-foreground">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</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-4 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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FAQ data
|
// FAQ data
|
||||||
@ -180,19 +310,9 @@ export function PublicInternetPlansContent({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Group services items by offering type
|
// Consolidated internet plans data - one card with all tiers
|
||||||
const groupedOfferings = useMemo(() => {
|
const consolidatedPlanData = useMemo(() => {
|
||||||
if (!servicesCatalog?.plans) return [];
|
if (!servicesCatalog?.plans) return null;
|
||||||
|
|
||||||
const plansByType = servicesCatalog.plans.reduce(
|
|
||||||
(acc, plan) => {
|
|
||||||
const key = plan.internetOfferingType ?? "unknown";
|
|
||||||
if (!acc[key]) acc[key] = [];
|
|
||||||
acc[key].push(plan);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, InternetPlanCatalogItem[]>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get installation item for setup fee
|
// Get installation item for setup fee
|
||||||
const installationItem = servicesCatalog.installations?.[0] as
|
const installationItem = servicesCatalog.installations?.[0] as
|
||||||
@ -200,147 +320,47 @@ export function PublicInternetPlansContent({
|
|||||||
| undefined;
|
| undefined;
|
||||||
const setupFee = installationItem?.oneTimePrice ?? 22800;
|
const setupFee = installationItem?.oneTimePrice ?? 22800;
|
||||||
|
|
||||||
// Create grouped offerings
|
// Get all prices across all plan types to show full range
|
||||||
const offerings: GroupedOffering[] = [];
|
const allPrices = servicesCatalog.plans.map(p => p.monthlyPrice ?? 0).filter(p => p > 0);
|
||||||
|
const minPrice = Math.min(...allPrices);
|
||||||
|
const maxPrice = Math.max(...allPrices);
|
||||||
|
|
||||||
// Consolidate apartment types (they all have the same price)
|
// Get unique tiers with their price ranges across all offering types
|
||||||
// Connection type (FTTH, VDSL, LAN) depends on building infrastructure
|
const tierOrder: Record<string, number> = { Silver: 0, Gold: 1, Platinum: 2 };
|
||||||
const apartmentTypes = ["Apartment 1G", "Apartment 100M"];
|
const tierData: Record<
|
||||||
const apartmentPlans: InternetPlanCatalogItem[] = [];
|
|
||||||
|
|
||||||
for (const type of apartmentTypes) {
|
|
||||||
if (plansByType[type]) {
|
|
||||||
apartmentPlans.push(...plansByType[type]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define offering metadata
|
|
||||||
// Order: Home 10G first (premium), then Home 1G, then consolidated Apartment
|
|
||||||
const offeringMeta: Record<
|
|
||||||
string,
|
string,
|
||||||
{
|
{ minPrice: number; maxPrice: number; plans: InternetPlanCatalogItem[] }
|
||||||
title: string;
|
> = {};
|
||||||
description: string;
|
|
||||||
iconType: "home" | "apartment";
|
for (const plan of servicesCatalog.plans) {
|
||||||
order: number;
|
const tier = plan.internetPlanTier ?? "Silver";
|
||||||
isPremium?: boolean;
|
if (!tierData[tier]) {
|
||||||
|
tierData[tier] = { minPrice: Infinity, maxPrice: 0, plans: [] };
|
||||||
}
|
}
|
||||||
> = {
|
const price = plan.monthlyPrice ?? 0;
|
||||||
"Home 10G": {
|
tierData[tier].minPrice = Math.min(tierData[tier].minPrice, price);
|
||||||
title: "Home 10Gbps",
|
tierData[tier].maxPrice = Math.max(tierData[tier].maxPrice, price);
|
||||||
description: "Ultra-fast fiber with the highest speeds available in Japan.",
|
tierData[tier].plans.push(plan);
|
||||||
iconType: "home",
|
}
|
||||||
order: 1,
|
|
||||||
isPremium: true,
|
// Build consolidated tier info with price ranges
|
||||||
},
|
const tiers: TierInfo[] = Object.entries(tierData)
|
||||||
"Home 1G": {
|
.sort(([a], [b]) => (tierOrder[a] ?? 99) - (tierOrder[b] ?? 99))
|
||||||
title: "Home 1Gbps",
|
.map(([tier, data]) => ({
|
||||||
description: "High-speed fiber. The most popular choice for home internet.",
|
tier: tier as TierInfo["tier"],
|
||||||
iconType: "home",
|
monthlyPrice: data.minPrice,
|
||||||
order: 2,
|
maxMonthlyPrice: data.maxPrice,
|
||||||
},
|
description: getTierDescription(tier),
|
||||||
Apartment: {
|
features: getTierFeatures(tier),
|
||||||
title: "Apartment",
|
pricingNote: tier === "Platinum" ? "+ equipment fees" : undefined,
|
||||||
description:
|
}));
|
||||||
"For mansions and apartment buildings. Speed depends on your building (up to 1Gbps).",
|
|
||||||
iconType: "apartment",
|
return {
|
||||||
order: 3,
|
minPrice,
|
||||||
},
|
maxPrice,
|
||||||
|
setupFee,
|
||||||
|
tiers,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process Home offerings
|
|
||||||
for (const [offeringType, plans] of Object.entries(plansByType)) {
|
|
||||||
// Skip apartment types - we'll handle them separately
|
|
||||||
if (apartmentTypes.includes(offeringType)) continue;
|
|
||||||
|
|
||||||
const meta = offeringMeta[offeringType];
|
|
||||||
if (!meta) continue;
|
|
||||||
|
|
||||||
// Sort plans by tier: Silver, Gold, Platinum
|
|
||||||
const tierOrder: Record<string, number> = { Silver: 0, Gold: 1, Platinum: 2 };
|
|
||||||
const sortedPlans = [...plans].sort(
|
|
||||||
(a, b) =>
|
|
||||||
(tierOrder[a.internetPlanTier ?? ""] ?? 99) - (tierOrder[b.internetPlanTier ?? ""] ?? 99)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate starting price
|
|
||||||
const startingPrice = Math.min(...sortedPlans.map(p => p.monthlyPrice ?? 0));
|
|
||||||
|
|
||||||
// Get speed from offering type
|
|
||||||
const speedBadge = getSpeedBadge(offeringType);
|
|
||||||
|
|
||||||
// Build tier info (no recommended badge in public view)
|
|
||||||
const tiers: TierInfo[] = sortedPlans.map(plan => ({
|
|
||||||
tier: (plan.internetPlanTier ?? "Silver") as TierInfo["tier"],
|
|
||||||
monthlyPrice: plan.monthlyPrice ?? 0,
|
|
||||||
description: getTierDescription(plan.internetPlanTier ?? ""),
|
|
||||||
features: plan.catalogMetadata?.features ?? getTierFeatures(plan.internetPlanTier ?? ""),
|
|
||||||
pricingNote: plan.internetPlanTier === "Platinum" ? "+ equipment fees" : undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
offerings.push({
|
|
||||||
offeringType,
|
|
||||||
title: meta.title,
|
|
||||||
speedBadge,
|
|
||||||
description: meta.description,
|
|
||||||
iconType: meta.iconType,
|
|
||||||
startingPrice,
|
|
||||||
setupFee,
|
|
||||||
tiers,
|
|
||||||
isPremium: meta.isPremium,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add consolidated Apartment offering (use any apartment plan for tiers - prices are the same)
|
|
||||||
if (apartmentPlans.length > 0) {
|
|
||||||
const meta = offeringMeta["Apartment"];
|
|
||||||
|
|
||||||
// Get unique tiers from apartment plans (they all have same prices)
|
|
||||||
const tierOrder: Record<string, number> = { Silver: 0, Gold: 1, Platinum: 2 };
|
|
||||||
const uniqueTiers = new Map<string, InternetPlanCatalogItem>();
|
|
||||||
|
|
||||||
for (const plan of apartmentPlans) {
|
|
||||||
const tier = plan.internetPlanTier ?? "Silver";
|
|
||||||
// Keep first occurrence of each tier (prices are same across apartment types)
|
|
||||||
if (!uniqueTiers.has(tier)) {
|
|
||||||
uniqueTiers.set(tier, plan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedTierPlans = Array.from(uniqueTiers.values()).sort(
|
|
||||||
(a, b) =>
|
|
||||||
(tierOrder[a.internetPlanTier ?? ""] ?? 99) - (tierOrder[b.internetPlanTier ?? ""] ?? 99)
|
|
||||||
);
|
|
||||||
|
|
||||||
const startingPrice = Math.min(...sortedTierPlans.map(p => p.monthlyPrice ?? 0));
|
|
||||||
|
|
||||||
const tiers: TierInfo[] = sortedTierPlans.map(plan => ({
|
|
||||||
tier: (plan.internetPlanTier ?? "Silver") as TierInfo["tier"],
|
|
||||||
monthlyPrice: plan.monthlyPrice ?? 0,
|
|
||||||
description: getTierDescription(plan.internetPlanTier ?? ""),
|
|
||||||
features: plan.catalogMetadata?.features ?? getTierFeatures(plan.internetPlanTier ?? ""),
|
|
||||||
pricingNote: plan.internetPlanTier === "Platinum" ? "+ equipment fees" : undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
offerings.push({
|
|
||||||
offeringType: "Apartment",
|
|
||||||
title: meta.title,
|
|
||||||
speedBadge: "Up to 1Gbps",
|
|
||||||
description: meta.description,
|
|
||||||
iconType: meta.iconType,
|
|
||||||
startingPrice,
|
|
||||||
setupFee,
|
|
||||||
tiers,
|
|
||||||
showConnectionInfo: true, // Show the info tooltip for Apartment
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by order
|
|
||||||
return offerings.sort((a, b) => {
|
|
||||||
const orderA = offeringMeta[a.offeringType]?.order ?? 99;
|
|
||||||
const orderB = offeringMeta[b.offeringType]?.order ?? 99;
|
|
||||||
return orderA - orderB;
|
|
||||||
});
|
|
||||||
}, [servicesCatalog]);
|
}, [servicesCatalog]);
|
||||||
|
|
||||||
// Error state
|
// Error state
|
||||||
@ -371,37 +391,21 @@ export function PublicInternetPlansContent({
|
|||||||
{/* Service Highlights */}
|
{/* Service Highlights */}
|
||||||
<ServiceHighlights features={internetFeatures} />
|
<ServiceHighlights features={internetFeatures} />
|
||||||
|
|
||||||
{/* Connection types - no extra header text */}
|
{/* Consolidated Internet Plans Card */}
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-3">
|
<Skeleton className="h-64 w-full rounded-xl" />
|
||||||
{[1, 2, 3].map(i => (
|
) : consolidatedPlanData ? (
|
||||||
<Skeleton key={i} className="h-28 w-full rounded-xl" />
|
<ConsolidatedInternetCard
|
||||||
))}
|
minPrice={consolidatedPlanData.minPrice}
|
||||||
</div>
|
maxPrice={consolidatedPlanData.maxPrice}
|
||||||
) : (
|
setupFee={consolidatedPlanData.setupFee}
|
||||||
<div className="space-y-3">
|
tiers={consolidatedPlanData.tiers}
|
||||||
{groupedOfferings.map((offering, index) => (
|
ctaPath={ctaPath}
|
||||||
<PublicOfferingCard
|
ctaLabel={ctaLabel}
|
||||||
key={offering.offeringType}
|
onCtaClick={onCtaClick}
|
||||||
offeringType={offering.offeringType}
|
/>
|
||||||
title={offering.title}
|
) : null}
|
||||||
speedBadge={offering.speedBadge}
|
|
||||||
description={offering.description}
|
|
||||||
iconType={offering.iconType}
|
|
||||||
startingPrice={offering.startingPrice}
|
|
||||||
setupFee={offering.setupFee}
|
|
||||||
tiers={offering.tiers}
|
|
||||||
isPremium={offering.isPremium}
|
|
||||||
ctaPath={ctaPath}
|
|
||||||
customCtaLabel={ctaLabel}
|
|
||||||
onCtaClick={onCtaClick}
|
|
||||||
defaultExpanded={index === 0}
|
|
||||||
showConnectionInfo={offering.showConnectionInfo}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Final CTA - Polished */}
|
{/* Final CTA - Polished */}
|
||||||
@ -453,16 +457,6 @@ export function PublicInternetPlansView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
function getSpeedBadge(offeringType: string): string {
|
|
||||||
const speeds: Record<string, string> = {
|
|
||||||
"Apartment 100M": "100Mbps",
|
|
||||||
"Apartment 1G": "1Gbps",
|
|
||||||
"Home 1G": "1Gbps",
|
|
||||||
"Home 10G": "10Gbps",
|
|
||||||
};
|
|
||||||
return speeds[offeringType] ?? "1Gbps";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTierDescription(tier: string): string {
|
function getTierDescription(tier: string): string {
|
||||||
const descriptions: Record<string, string> = {
|
const descriptions: Record<string, string> = {
|
||||||
Silver: "Use your own router. Best for tech-savvy users.",
|
Silver: "Use your own router. Best for tech-savvy users.",
|
||||||
@ -474,9 +468,21 @@ function getTierDescription(tier: string): string {
|
|||||||
|
|
||||||
function getTierFeatures(tier: string): string[] {
|
function getTierFeatures(tier: string): string[] {
|
||||||
const features: Record<string, string[]> = {
|
const features: Record<string, string[]> = {
|
||||||
Silver: ["NTT modem + ISP connection", "Use your own router", "Email/ticket support"],
|
Silver: [
|
||||||
Gold: ["NTT modem + ISP connection", "WiFi router included", "Priority phone support"],
|
"NTT modem + ISP connection",
|
||||||
Platinum: ["NTT modem + ISP connection", "Mesh WiFi system included", "Dedicated support line"],
|
"Two ISP connection protocols: IPoE (recommended) or PPPoE",
|
||||||
|
"Self-configuration of router (you provide your own)",
|
||||||
|
],
|
||||||
|
Gold: [
|
||||||
|
"NTT modem + wireless router (rental)",
|
||||||
|
"ISP (IPoE) configured automatically within 24 hours",
|
||||||
|
"Basic wireless router included",
|
||||||
|
],
|
||||||
|
Platinum: [
|
||||||
|
"NTT modem + Netgear INSIGHT Wi-Fi routers",
|
||||||
|
"Cloud management support for remote router management",
|
||||||
|
"Automatic updates and quicker support",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
return features[tier] ?? [];
|
return features[tier] ?? [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ShieldCheck, Zap } from "lucide-react";
|
import { useState } from "react";
|
||||||
|
import { ShieldCheck, Zap, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { usePublicVpnCatalog } from "@/features/services/hooks";
|
import { usePublicVpnCatalog } from "@/features/services/hooks";
|
||||||
import { LoadingCard } from "@/components/atoms";
|
import { LoadingCard } from "@/components/atoms";
|
||||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
||||||
@ -120,6 +121,9 @@ export function PublicVpnPlansView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
<VpnFaqSection />
|
||||||
|
|
||||||
<AlertBanner variant="warning" title="Important Disclaimer" className="mb-8">
|
<AlertBanner variant="warning" title="Important Disclaimer" className="mb-8">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
Content subscriptions are NOT included in the VPN package. Our VPN service establishes a
|
Content subscriptions are NOT included in the VPN package. Our VPN service establishes a
|
||||||
@ -132,4 +136,64 @@ export function PublicVpnPlansView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VPN FAQ Data
|
||||||
|
const vpnFaqItems = [
|
||||||
|
{
|
||||||
|
question: "What devices can I connect to the VPN router?",
|
||||||
|
answer:
|
||||||
|
"Any device that connects via Wi-Fi can use the VPN router, including Apple TV, Roku, Amazon Fire TV, gaming consoles, smart TVs, and computers. Simply connect to the VPN router's Wi-Fi network to route your traffic through the VPN server.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can I use VPN on my phone or laptop directly?",
|
||||||
|
answer:
|
||||||
|
"The VPN router service is designed for devices that don't natively support VPN apps. For phones and laptops, you could connect them to the VPN router's Wi-Fi, but we recommend using a standard VPN app on those devices for better flexibility.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What internet speeds can I expect through the VPN?",
|
||||||
|
answer:
|
||||||
|
"VPN speeds depend on your base internet connection and the distance to the VPN server. Typically, you can expect 20-100 Mbps for streaming, which is sufficient for 4K content. The San Francisco server generally offers faster speeds for users in Japan.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can I switch between regions after signing up?",
|
||||||
|
answer:
|
||||||
|
"Each router is pre-configured for one region (San Francisco or London). If you need to access content from both regions, you would need two separate router rentals. Contact us if you need to change your region assignment.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What happens if the VPN router breaks or stops working?",
|
||||||
|
answer:
|
||||||
|
"We provide technical support and will replace faulty equipment at no extra charge. Simply contact our support team and we'll arrange a replacement router to be shipped to you.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function VpnFaqSection() {
|
||||||
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-card rounded-xl border border-border p-8 mb-8">
|
||||||
|
<h2 className="text-xl font-bold text-foreground mb-6">Frequently Asked Questions</h2>
|
||||||
|
<div className="space-y-0 divide-y divide-border">
|
||||||
|
{vpnFaqItems.map((item, index) => (
|
||||||
|
<div key={index} className="py-4 first:pt-0 last:pb-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenIndex(openIndex === index ? null : index)}
|
||||||
|
className="w-full flex items-start justify-between gap-3 text-left hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<span className="font-medium text-foreground">{item.question}</span>
|
||||||
|
{openIndex === index ? (
|
||||||
|
<ChevronUp className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{openIndex === index && (
|
||||||
|
<p className="mt-3 text-sm text-muted-foreground leading-relaxed">{item.answer}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default PublicVpnPlansView;
|
export default PublicVpnPlansView;
|
||||||
|
|||||||
@ -52,6 +52,7 @@ function buildCSP(nonce: string, isDev: boolean): string {
|
|||||||
"img-src 'self' data: https:",
|
"img-src 'self' data: https:",
|
||||||
"font-src 'self' data:",
|
"font-src 'self' data:",
|
||||||
"connect-src 'self' https: http://localhost:* ws://localhost:*",
|
"connect-src 'self' https: http://localhost:* ws://localhost:*",
|
||||||
|
"frame-src 'self' https://www.google.com",
|
||||||
"frame-ancestors 'none'",
|
"frame-ancestors 'none'",
|
||||||
].join("; ");
|
].join("; ");
|
||||||
}
|
}
|
||||||
@ -67,6 +68,7 @@ function buildCSP(nonce: string, isDev: boolean): string {
|
|||||||
"img-src 'self' data: https:",
|
"img-src 'self' data: https:",
|
||||||
"font-src 'self' data:",
|
"font-src 'self' data:",
|
||||||
"connect-src 'self' https:",
|
"connect-src 'self' https:",
|
||||||
|
"frame-src 'self' https://www.google.com",
|
||||||
"frame-ancestors 'none'",
|
"frame-ancestors 'none'",
|
||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"form-action 'self'",
|
"form-action 'self'",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user