fix: resolve SIM management modal, plan change, and voice feature issues
- Fix Tailwind v4 modal stacking bug by adding relative z-10 to modal content divs (CancellationFlow, ChangePlanModal, TopUpModal, SimActions) - Add test mode for immediate plan changes (SIM_BILLING_TEST_MODE) instead of scheduling for 1st of next month - Bypass rate limiter spacing/cancellation checks in test mode - Hide voice feature toggles for data-only SIMs using hasVoice flag - Guard BFF voice feature updates to reject early for data-only SIMs - Fix Freebit retry logic to not retry business errors (e.g. resultCode 260) - Add user-friendly error message for resultCode 260 (voice not active) - Update plan change page text to reflect test mode behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
191a377657
commit
df017d520f
@ -49,13 +49,27 @@ export class FreebitError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if error is retryable
|
* Check if error is retryable.
|
||||||
|
*
|
||||||
|
* IMPORTANT: Freebit uses statusCode "500" for business logic errors (e.g. 210, 211, 260),
|
||||||
|
* not just HTTP 500 server errors. Only resultCode 900 (unexpected error) is truly retryable.
|
||||||
|
* We should NOT retry known business errors even though their statusCode is "500".
|
||||||
*/
|
*/
|
||||||
isRetryable(): boolean {
|
isRetryable(): boolean {
|
||||||
const retryableCodes = ["500", "502", "503", "504", "408", "429"];
|
// Freebit-specific: only retry unexpected errors (900) or actual HTTP transport errors
|
||||||
|
if (this.resultCode === "900") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a Freebit resultCode, it's a business error — don't retry
|
||||||
|
if (this.resultCode && this.resultCode !== "900") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-Freebit errors (HTTP transport failures), use standard retry logic
|
||||||
|
const retryableHttpCodes = ["500", "502", "503", "504", "408", "429"];
|
||||||
return (
|
return (
|
||||||
retryableCodes.includes(String(this.resultCode)) ||
|
retryableHttpCodes.includes(String(this.statusCode)) ||
|
||||||
retryableCodes.includes(String(this.statusCode)) ||
|
|
||||||
this.message.toLowerCase().includes("timeout") ||
|
this.message.toLowerCase().includes("timeout") ||
|
||||||
this.message.toLowerCase().includes("network")
|
this.message.toLowerCase().includes("network")
|
||||||
);
|
);
|
||||||
@ -94,6 +108,10 @@ export class FreebitError extends Error {
|
|||||||
return "Plan change failed. This may be due to: (1) Account has existing scheduled operations, (2) Invalid plan code for this account, (3) Account restrictions. Please check the Freebit Partner Tools for account status or contact support.";
|
return "Plan change failed. This may be due to: (1) Account has existing scheduled operations, (2) Invalid plan code for this account, (3) Account restrictions. Please check the Freebit Partner Tools for account status or contact support.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.resultCode === "260" || this.statusCode === "260") {
|
||||||
|
return "Voice options cannot be changed because voice service is not active on this account. Voice options must first be registered via the initial setup process.";
|
||||||
|
}
|
||||||
|
|
||||||
if (this.resultCode === "381" || this.statusCode === "381") {
|
if (this.resultCode === "381" || this.statusCode === "381") {
|
||||||
return "Network type change rejected. The current plan does not allow switching to the requested contract line. Adjust the plan first or contact support.";
|
return "Network type change rejected. The current plan does not allow switching to the requested contract line. Adjust the plan first or contact support.";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Injectable, Inject, BadRequestException } from "@nestjs/common";
|
import { Injectable, Inject, BadRequestException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import type { Redis } from "ioredis";
|
import type { Redis } from "ioredis";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
@ -32,10 +33,20 @@ export class FreebitRateLimiterService {
|
|||||||
private readonly windowMs = 30 * 60 * 1000; // 30 minute window between operations
|
private readonly windowMs = 30 * 60 * 1000; // 30 minute window between operations
|
||||||
private readonly lockTtlMs = Math.min(this.windowMs, 10 * 60 * 1000);
|
private readonly lockTtlMs = Math.min(this.windowMs, 10 * 60 * 1000);
|
||||||
|
|
||||||
|
private readonly testMode: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject("REDIS_CLIENT") private readonly redis: Redis,
|
@Inject("REDIS_CLIENT") private readonly redis: Redis,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
@Inject(Logger) private readonly logger: Logger
|
@Inject(Logger) private readonly logger: Logger
|
||||||
) {}
|
) {
|
||||||
|
this.testMode = this.configService.get<string>("SIM_BILLING_TEST_MODE") === "true";
|
||||||
|
if (this.testMode) {
|
||||||
|
this.logger.warn(
|
||||||
|
"Freebit rate limiter is in TEST MODE - spacing checks and cancellation blocks are bypassed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup stale entries from operationTimestamps to prevent memory leak.
|
* Cleanup stale entries from operationTimestamps to prevent memory leak.
|
||||||
@ -174,6 +185,13 @@ export class FreebitRateLimiterService {
|
|||||||
* Throws BadRequestException if the operation violates timing rules.
|
* Throws BadRequestException if the operation violates timing rules.
|
||||||
*/
|
*/
|
||||||
async assertOperationSpacing(account: string, op: OperationType): Promise<void> {
|
async assertOperationSpacing(account: string, op: OperationType): Promise<void> {
|
||||||
|
if (this.testMode) {
|
||||||
|
this.logger.debug(
|
||||||
|
`TEST MODE: Skipping operation spacing check for ${op} on account ${account}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const entry = await this.getOperationWindow(account);
|
const entry = await this.getOperationWindow(account);
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,8 @@ const FREEBIT_PLAN_CODE_TO_SKU: Record<string, string> = Object.fromEntries(
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SimPlanService {
|
export class SimPlanService {
|
||||||
|
private readonly testMode: boolean;
|
||||||
|
|
||||||
// eslint-disable-next-line max-params -- NestJS dependency injection requires all services to be injected via constructor
|
// eslint-disable-next-line max-params -- NestJS dependency injection requires all services to be injected via constructor
|
||||||
constructor(
|
constructor(
|
||||||
private readonly freebitService: FreebitFacade,
|
private readonly freebitService: FreebitFacade,
|
||||||
@ -44,7 +46,14 @@ export class SimPlanService {
|
|||||||
private readonly simCatalog: SimServicesService,
|
private readonly simCatalog: SimServicesService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
@Inject(Logger) private readonly logger: Logger
|
@Inject(Logger) private readonly logger: Logger
|
||||||
) {}
|
) {
|
||||||
|
this.testMode = this.configService.get<string>("SIM_BILLING_TEST_MODE") === "true";
|
||||||
|
if (this.testMode) {
|
||||||
|
this.logger.warn(
|
||||||
|
"SIM Plan service is in TEST MODE - plan changes will take effect immediately"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private get freebitBaseUrl(): string {
|
private get freebitBaseUrl(): string {
|
||||||
return this.configService.get<string>("FREEBIT_BASE_URL") || "https://i1.mvno.net/emptool/api";
|
return this.configService.get<string>("FREEBIT_BASE_URL") || "https://i1.mvno.net/emptool/api";
|
||||||
@ -194,13 +203,22 @@ export class SimPlanService {
|
|||||||
throw new BadRequestException("Invalid plan code");
|
throw new BadRequestException("Invalid plan code");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always schedule for 1st of following month
|
// In test mode, schedule for today (immediate effect); otherwise 1st of following month
|
||||||
const nextMonth = new Date();
|
let scheduledAt: string;
|
||||||
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
if (this.testMode) {
|
||||||
nextMonth.setDate(1);
|
const today = new Date();
|
||||||
const year = nextMonth.getFullYear();
|
const year = today.getFullYear();
|
||||||
const month = String(nextMonth.getMonth() + 1).padStart(2, "0");
|
const month = String(today.getMonth() + 1).padStart(2, "0");
|
||||||
const scheduledAt = `${year}${month}01`;
|
const day = String(today.getDate()).padStart(2, "0");
|
||||||
|
scheduledAt = `${year}${month}${day}`;
|
||||||
|
} else {
|
||||||
|
const nextMonth = new Date();
|
||||||
|
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
||||||
|
nextMonth.setDate(1);
|
||||||
|
const year = nextMonth.getFullYear();
|
||||||
|
const month = String(nextMonth.getMonth() + 1).padStart(2, "0");
|
||||||
|
scheduledAt = `${year}${month}01`;
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.log("Submitting SIM plan change request (full)", {
|
this.logger.log("Submitting SIM plan change request (full)", {
|
||||||
userId,
|
userId,
|
||||||
@ -288,6 +306,16 @@ export class SimPlanService {
|
|||||||
const hasVoiceChanges = this.hasVoiceFeatureChanges(request);
|
const hasVoiceChanges = this.hasVoiceFeatureChanges(request);
|
||||||
const hasContractChange = typeof request.networkType === "string";
|
const hasContractChange = typeof request.networkType === "string";
|
||||||
|
|
||||||
|
// Guard: reject voice feature changes for data-only SIMs (voice not active)
|
||||||
|
if (hasVoiceChanges) {
|
||||||
|
const simDetails = await this.freebitService.getSimDetails(account);
|
||||||
|
if (!simDetails.hasVoice) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Voice features cannot be changed because voice service is not active on this data-only SIM."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasVoiceChanges && hasContractChange) {
|
if (hasVoiceChanges && hasContractChange) {
|
||||||
await this.applyVoiceThenContractChanges(account, request, userId, subscriptionId);
|
await this.applyVoiceThenContractChanges(account, request, userId, subscriptionId);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,109 +1,513 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Button } from "@/components/atoms";
|
import { Button } from "@/components/atoms";
|
||||||
import { Server, Monitor, Wrench, Globe } from "lucide-react";
|
import Link from "next/link";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "IT Solutions for International Businesses in Japan | Assist Solutions",
|
title: "IT Solutions for International Businesses in Japan | Assist Solutions",
|
||||||
description:
|
description:
|
||||||
"Enterprise IT for foreign companies in Japan. Dedicated internet, office networks, data center hosting, all with bilingual support. We understand international business needs.",
|
"Enterprise IT for foreign companies in Japan. Dedicated internet, office networks, data center hosting, web construction & maintenance, all with bilingual support.",
|
||||||
keywords: [
|
keywords: [
|
||||||
"IT for foreign companies Japan",
|
"IT for foreign companies Japan",
|
||||||
"international business IT Tokyo",
|
"international business IT Tokyo",
|
||||||
"bilingual IT support Japan",
|
"bilingual IT support Japan",
|
||||||
"office network setup foreigners",
|
"office network setup foreigners",
|
||||||
"enterprise IT English support",
|
"enterprise IT English support",
|
||||||
|
"web design Japan English",
|
||||||
|
"website maintenance Tokyo",
|
||||||
|
"bilingual web development",
|
||||||
],
|
],
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Business IT for International Companies - Assist Solutions",
|
title: "Business IT for International Companies - Assist Solutions",
|
||||||
description:
|
description:
|
||||||
"Enterprise IT with bilingual support. Dedicated internet, office networks, and data center services for foreign companies in Japan.",
|
"Enterprise IT with bilingual support. Dedicated internet, office networks, web construction, and data center services for foreign companies in Japan.",
|
||||||
type: "website",
|
type: "website",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BusinessSolutionsPage() {
|
// Abstract tech illustration components
|
||||||
|
function NetworkNodesIllustration({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto px-4">
|
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
{/* Header */}
|
<circle cx="100" cy="100" r="8" fill="currentColor" fillOpacity="0.3" />
|
||||||
<div className="text-center mb-16 pt-8">
|
<circle cx="40" cy="60" r="6" fill="currentColor" fillOpacity="0.2" />
|
||||||
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-6 tracking-tight">
|
<circle cx="160" cy="50" r="5" fill="currentColor" fillOpacity="0.25" />
|
||||||
IT for International Businesses
|
<circle cx="150" cy="140" r="7" fill="currentColor" fillOpacity="0.2" />
|
||||||
</h1>
|
<circle cx="50" cy="150" r="5" fill="currentColor" fillOpacity="0.25" />
|
||||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
<circle cx="170" cy="100" r="4" fill="currentColor" fillOpacity="0.15" />
|
||||||
Running an international company in Japan? We provide enterprise IT with bilingual
|
<circle cx="30" cy="110" r="4" fill="currentColor" fillOpacity="0.15" />
|
||||||
support, so your team can focus on business, not navigating Japanese tech providers.
|
<line
|
||||||
</p>
|
x1="100"
|
||||||
|
y1="100"
|
||||||
|
x2="40"
|
||||||
|
y2="60"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="100"
|
||||||
|
y1="100"
|
||||||
|
x2="160"
|
||||||
|
y2="50"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="100"
|
||||||
|
y1="100"
|
||||||
|
x2="150"
|
||||||
|
y2="140"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="100"
|
||||||
|
y1="100"
|
||||||
|
x2="50"
|
||||||
|
y2="150"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="100"
|
||||||
|
y1="100"
|
||||||
|
x2="170"
|
||||||
|
y2="100"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.1"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="100"
|
||||||
|
y1="100"
|
||||||
|
x2="30"
|
||||||
|
y2="110"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.1"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="100"
|
||||||
|
cy="100"
|
||||||
|
r="40"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.08"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="100"
|
||||||
|
cy="100"
|
||||||
|
r="70"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.05"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GearIllustration({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M100 130c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30-16.569 0-30 13.431-30 30 0 16.569 13.431 30 30 30z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.2"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M100 60V40M100 160v-20M140 100h20M40 100h20M128.28 71.72l14.14-14.14M57.58 142.42l14.14-14.14M128.28 128.28l14.14 14.14M57.58 57.58l14.14 14.14"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<circle cx="100" cy="100" r="15" stroke="currentColor" strokeOpacity="0.25" strokeWidth="2" />
|
||||||
|
<circle
|
||||||
|
cx="155"
|
||||||
|
cy="155"
|
||||||
|
r="20"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.1"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<circle cx="155" cy="155" r="8" fill="currentColor" fillOpacity="0.08" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShieldIllustration({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M100 30L40 55v45c0 38.66 25.64 74.64 60 85 34.36-10.36 60-46.34 60-85V55L100 30z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="currentColor"
|
||||||
|
fillOpacity="0.03"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M100 50L55 70v35c0 30.13 19.98 58.15 45 66.25V50z"
|
||||||
|
fill="currentColor"
|
||||||
|
fillOpacity="0.05"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M85 100l10 10 20-25"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.25"
|
||||||
|
strokeWidth="3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeIllustration({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect
|
||||||
|
x="30"
|
||||||
|
y="40"
|
||||||
|
width="140"
|
||||||
|
height="100"
|
||||||
|
rx="8"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="30"
|
||||||
|
y1="60"
|
||||||
|
x2="170"
|
||||||
|
y2="60"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.1"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
<circle cx="45" cy="50" r="4" fill="currentColor" fillOpacity="0.15" />
|
||||||
|
<circle cx="60" cy="50" r="4" fill="currentColor" fillOpacity="0.15" />
|
||||||
|
<circle cx="75" cy="50" r="4" fill="currentColor" fillOpacity="0.15" />
|
||||||
|
<path
|
||||||
|
d="M70 95l-20 15 20 15"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.2"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M130 95l20 15-20 15"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.2"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="90"
|
||||||
|
y1="130"
|
||||||
|
x2="110"
|
||||||
|
y2="80"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.25"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServerIllustration({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect
|
||||||
|
x="50"
|
||||||
|
y="30"
|
||||||
|
width="100"
|
||||||
|
height="35"
|
||||||
|
rx="4"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.2"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="currentColor"
|
||||||
|
fillOpacity="0.03"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="50"
|
||||||
|
y="75"
|
||||||
|
width="100"
|
||||||
|
height="35"
|
||||||
|
rx="4"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.2"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="currentColor"
|
||||||
|
fillOpacity="0.03"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="50"
|
||||||
|
y="120"
|
||||||
|
width="100"
|
||||||
|
height="35"
|
||||||
|
rx="4"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.2"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="currentColor"
|
||||||
|
fillOpacity="0.03"
|
||||||
|
/>
|
||||||
|
<circle cx="65" cy="47" r="4" fill="currentColor" fillOpacity="0.3" />
|
||||||
|
<circle cx="65" cy="92" r="4" fill="currentColor" fillOpacity="0.2" />
|
||||||
|
<circle cx="65" cy="137" r="4" fill="currentColor" fillOpacity="0.25" />
|
||||||
|
<line
|
||||||
|
x1="80"
|
||||||
|
y1="47"
|
||||||
|
x2="130"
|
||||||
|
y2="47"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.1"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="80"
|
||||||
|
y1="92"
|
||||||
|
x2="120"
|
||||||
|
y2="92"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.1"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="80"
|
||||||
|
y1="137"
|
||||||
|
x2="125"
|
||||||
|
y2="137"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.1"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M100 165v20M85 185h30"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service card component
|
||||||
|
interface ServiceCardProps {
|
||||||
|
category: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
bgColor: string;
|
||||||
|
textColor: string;
|
||||||
|
accentColor: string;
|
||||||
|
illustration: React.ReactNode;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServiceCard({
|
||||||
|
category,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
bgColor,
|
||||||
|
textColor,
|
||||||
|
accentColor,
|
||||||
|
illustration,
|
||||||
|
href,
|
||||||
|
}: ServiceCardProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative overflow-hidden rounded-2xl ${bgColor} p-8 flex flex-col justify-between min-h-[420px] group transition-all duration-300 hover:-translate-y-1 border border-border/60 shadow-[0_4px_20px_-4px_rgba(0,0,0,0.08)] hover:shadow-[0_8px_30px_-4px_rgba(0,0,0,0.12)]`}
|
||||||
|
>
|
||||||
|
{/* Tech illustration overlay */}
|
||||||
|
<div className={`absolute top-0 right-0 w-48 h-48 ${accentColor} pointer-events-none`}>
|
||||||
|
{illustration}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-16">
|
{/* Content */}
|
||||||
{/* Office LAN Setup */}
|
<div className="relative z-10 flex flex-col h-full">
|
||||||
<div className="bg-card rounded-2xl border border-border/60 p-8 shadow-sm">
|
{/* Category label */}
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-6">
|
<span
|
||||||
<Monitor className="h-6 w-6" />
|
className={`text-xs font-semibold tracking-widest uppercase ${textColor} opacity-70 mb-4`}
|
||||||
</div>
|
>
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-4">Office LAN Setup</h2>
|
{category}
|
||||||
<p className="text-muted-foreground leading-relaxed mb-6">
|
</span>
|
||||||
Setting up a new office or upgrading your network? We handle everything in English. from
|
|
||||||
planning to installation. Cable runs, switches, routers, and firewalls configured by
|
|
||||||
bilingual technicians who understand international business needs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Onsite & Remote Tech Support */}
|
{/* Title */}
|
||||||
<div className="bg-card rounded-2xl border border-border/60 p-8 shadow-sm">
|
<h2
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-6">
|
className={`text-2xl lg:text-3xl font-bold ${textColor} mb-4 leading-tight max-w-[85%]`}
|
||||||
<Wrench className="h-6 w-6" />
|
>
|
||||||
</div>
|
{title}
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-4">Onsite & Remote Tech Support</h2>
|
|
||||||
<p className="text-muted-foreground leading-relaxed mb-6">
|
|
||||||
IT issues don't wait, and neither do we. Our English-speaking technicians provide
|
|
||||||
fast onsite and remote support for your business. Network problems, hardware issues,
|
|
||||||
software setup. We keep your operations running smoothly.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dedicated Internet Access (DIA) */}
|
|
||||||
<div className="bg-card rounded-2xl border border-border/60 p-8 shadow-sm">
|
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-6">
|
|
||||||
<Globe className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-4">
|
|
||||||
Dedicated Internet Access (DIA)
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground leading-relaxed mb-6">
|
|
||||||
Need guaranteed bandwidth for your business? Our Dedicated Internet Access provides
|
|
||||||
enterprise-grade connectivity with SLA guarantees. Perfect for companies requiring
|
|
||||||
reliable, high-capacity connections with English contracts and support.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Data Center Service */}
|
|
||||||
<div className="bg-card rounded-2xl border border-border/60 p-8 shadow-sm">
|
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-6">
|
|
||||||
<Server className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-4">Data Center Service</h2>
|
|
||||||
<p className="text-muted-foreground leading-relaxed mb-6">
|
|
||||||
Host your infrastructure in world-class Tokyo data centers (Equinix, GDC Gotenyama). We
|
|
||||||
provide colocation and managed services with English support, making it easy for
|
|
||||||
international companies to establish reliable IT infrastructure in Japan.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA */}
|
|
||||||
<div className="text-center py-12 bg-muted/20 rounded-3xl mb-16">
|
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-4">
|
|
||||||
Let's Talk About Your IT Needs
|
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mb-8 max-w-2xl mx-auto">
|
|
||||||
Running an international business in Japan comes with unique challenges. We've been
|
{/* Description */}
|
||||||
helping foreign companies navigate Japanese IT for over 20 years. Let's discuss how
|
<p className={`${textColor} opacity-80 text-sm leading-relaxed mb-8 max-w-[90%]`}>
|
||||||
we can support your operations.
|
{description}
|
||||||
</p>
|
</p>
|
||||||
<Button as="a" href="/contact" size="lg">
|
|
||||||
Get in Touch
|
{/* Spacer */}
|
||||||
</Button>
|
<div className="flex-grow" />
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={`inline-flex items-center gap-2 text-sm font-medium ${textColor} opacity-90 hover:opacity-100 transition-opacity group/link`}
|
||||||
|
>
|
||||||
|
Contact Us
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 transition-transform group-hover/link:translate-x-1"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BusinessSolutionsPage() {
|
||||||
|
const services: ServiceCardProps[] = [
|
||||||
|
{
|
||||||
|
category: "Infrastructure",
|
||||||
|
title: "Office LAN Setup",
|
||||||
|
description:
|
||||||
|
"Complete network infrastructure from planning to installation. Routers, switches, and firewalls configured by bilingual technicians.",
|
||||||
|
bgColor: "bg-[#1E3A5F]",
|
||||||
|
textColor: "text-white",
|
||||||
|
accentColor: "text-blue-300",
|
||||||
|
illustration: <NetworkNodesIllustration className="w-full h-full" />,
|
||||||
|
href: "/contact",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Support",
|
||||||
|
title: "Onsite & Remote Tech Support",
|
||||||
|
description:
|
||||||
|
"Fast response from English-speaking technicians. Network issues, hardware problems, software setup — we keep you running.",
|
||||||
|
bgColor: "bg-white",
|
||||||
|
textColor: "text-gray-900",
|
||||||
|
accentColor: "text-gray-400",
|
||||||
|
illustration: <GearIllustration className="w-full h-full" />,
|
||||||
|
href: "/contact",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Connectivity",
|
||||||
|
title: "Dedicated Internet Access",
|
||||||
|
description:
|
||||||
|
"Enterprise-grade connectivity with SLA guarantees. Reliable, high-capacity connections with English contracts and support.",
|
||||||
|
bgColor: "bg-[#F5F0E8]",
|
||||||
|
textColor: "text-gray-900",
|
||||||
|
accentColor: "text-amber-700",
|
||||||
|
illustration: <ShieldIllustration className="w-full h-full" />,
|
||||||
|
href: "/contact",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Hosting",
|
||||||
|
title: "Data Center Service",
|
||||||
|
description:
|
||||||
|
"World-class Tokyo data centers including Equinix and GDC Gotenyama. Colocation and managed services with full English support.",
|
||||||
|
bgColor: "bg-white",
|
||||||
|
textColor: "text-gray-900",
|
||||||
|
accentColor: "text-gray-400",
|
||||||
|
illustration: <ServerIllustration className="w-full h-full" />,
|
||||||
|
href: "/contact",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Digital",
|
||||||
|
title: "Web Construction & Maintenance",
|
||||||
|
description:
|
||||||
|
"Modern, responsive websites with bilingual support. From initial design to ongoing maintenance — your complete web partner.",
|
||||||
|
bgColor: "bg-[#1E3A5F]",
|
||||||
|
textColor: "text-white",
|
||||||
|
accentColor: "text-blue-300",
|
||||||
|
illustration: <CodeIllustration className="w-full h-full" />,
|
||||||
|
href: "/contact",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="bg-gradient-to-b from-muted/30 to-background">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-12 pb-16">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<p className="text-sm font-semibold tracking-widest uppercase text-primary mb-4">
|
||||||
|
Enterprise Solutions
|
||||||
|
</p>
|
||||||
|
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-foreground mb-6 leading-[1.1] tracking-tight">
|
||||||
|
IT for International
|
||||||
|
<br />
|
||||||
|
<span className="text-primary">Businesses in Japan</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-muted-foreground leading-relaxed max-w-2xl">
|
||||||
|
Running an international company in Japan? We provide enterprise IT with bilingual
|
||||||
|
support, so your team can focus on business — not navigating Japanese tech providers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services Card Grid */}
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-16">
|
||||||
|
{/* Desktop: Horizontal scroll layout */}
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<div className="grid grid-cols-5 gap-4">
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<ServiceCard key={index} {...service} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tablet: 2-column grid */}
|
||||||
|
<div className="hidden md:grid lg:hidden grid-cols-2 gap-4">
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<ServiceCard key={index} {...service} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile: Single column */}
|
||||||
|
<div className="md:hidden flex flex-col gap-4">
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<ServiceCard key={index} {...service} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom CTA Section */}
|
||||||
|
<div className="bg-muted/20">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||||
|
<div className="flex flex-col lg:flex-row items-center justify-between gap-8">
|
||||||
|
<div className="text-center lg:text-left">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-foreground mb-3">
|
||||||
|
Let's Talk About Your IT Needs
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground max-w-xl">
|
||||||
|
We've been helping foreign companies navigate Japanese IT for over 20 years.
|
||||||
|
Let's discuss how we can support your operations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button as="a" href="/contact" size="lg" className="whitespace-nowrap px-8">
|
||||||
|
Get in Touch
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,146 +1,427 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/atoms";
|
import { Button } from "@/components/atoms";
|
||||||
import { Users, Monitor, Tv, Headset, ChevronDown } from "lucide-react";
|
import {
|
||||||
|
Monitor,
|
||||||
|
Tv,
|
||||||
|
Headset,
|
||||||
|
ChevronDown,
|
||||||
|
Wrench,
|
||||||
|
Wifi,
|
||||||
|
Printer,
|
||||||
|
HardDrive,
|
||||||
|
CheckCircle2,
|
||||||
|
Calendar,
|
||||||
|
UserCheck,
|
||||||
|
ArrowRight,
|
||||||
|
Clock,
|
||||||
|
Shield,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
// Service types for the pricing cards
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
id: "onsite",
|
||||||
|
icon: Monitor,
|
||||||
|
title: "Onsite Support",
|
||||||
|
subtitle: "Home & Office Visits",
|
||||||
|
description:
|
||||||
|
"We come to your location for hands-on help with networks, computers, and devices.",
|
||||||
|
price: "15,000",
|
||||||
|
features: [
|
||||||
|
"Home or office visit",
|
||||||
|
"Network setup & troubleshooting",
|
||||||
|
"Device configuration",
|
||||||
|
"Same-week scheduling",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "remote",
|
||||||
|
icon: Headset,
|
||||||
|
title: "Remote Support",
|
||||||
|
subtitle: "Quick Online Help",
|
||||||
|
description: "We connect securely to your device to diagnose and fix issues remotely.",
|
||||||
|
price: "5,000",
|
||||||
|
popular: true,
|
||||||
|
features: [
|
||||||
|
"Secure remote connection",
|
||||||
|
"Quick turnaround",
|
||||||
|
"Software troubleshooting",
|
||||||
|
"No waiting for visit",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tv",
|
||||||
|
icon: Tv,
|
||||||
|
title: "TV & Streaming Setup",
|
||||||
|
subtitle: "Entertainment Systems",
|
||||||
|
description: "Setup and configuration of smart TVs, streaming devices, and home entertainment.",
|
||||||
|
price: "15,000",
|
||||||
|
features: [
|
||||||
|
"Smart TV setup",
|
||||||
|
"Streaming device config",
|
||||||
|
"Apple TV, Fire TV, etc.",
|
||||||
|
"Content access help",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// What we help with
|
||||||
|
const helpItems = [
|
||||||
|
{ icon: Wifi, label: "Wi-Fi & Routers" },
|
||||||
|
{ icon: Monitor, label: "Computers" },
|
||||||
|
{ icon: Printer, label: "Printers" },
|
||||||
|
{ icon: Tv, label: "Smart TVs" },
|
||||||
|
{ icon: HardDrive, label: "Storage & Backup" },
|
||||||
|
{ icon: Wrench, label: "General Tech" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// How it works steps
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
number: "1",
|
||||||
|
icon: Calendar,
|
||||||
|
title: "Book a Visit",
|
||||||
|
description:
|
||||||
|
"Contact us to schedule a time that works for you. Same-week appointments available.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "2",
|
||||||
|
icon: UserCheck,
|
||||||
|
title: "We Come to You",
|
||||||
|
description:
|
||||||
|
"An English-speaking technician arrives at your home or office at the scheduled time.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "3",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
title: "Problem Solved",
|
||||||
|
description: "We fix the issue and explain everything clearly so you understand what was done.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export function OnsiteSupportContent() {
|
export function OnsiteSupportContent() {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto px-4">
|
<div className="relative">
|
||||||
{/* Header */}
|
{/* Hero Section */}
|
||||||
<div className="text-center mb-16 pt-8">
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 overflow-hidden bg-gradient-to-br from-slate-50 via-white to-sky-50/80 pt-10 pb-20">
|
||||||
<h1 className="text-4xl sm:text-5xl font-extrabold text-foreground mb-6 tracking-tight">
|
{/* Dot grid pattern */}
|
||||||
Tech Help in English, At Your Door
|
<div
|
||||||
</h1>
|
className="absolute inset-0 pointer-events-none"
|
||||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
style={{
|
||||||
Need help with your router, computer, or home network? Our English-speaking technicians
|
backgroundImage: `radial-gradient(circle at center, oklch(0.65 0.05 234.4 / 0.12) 1px, transparent 1px)`,
|
||||||
come to your home or office to solve tech problems, explained in a language you
|
backgroundSize: "24px 24px",
|
||||||
understand.
|
}}
|
||||||
</p>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Services */}
|
{/* Gradient orb accents */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 mb-20">
|
<div
|
||||||
<div className="space-y-6">
|
className="absolute -top-32 -right-32 w-96 h-96 rounded-full pointer-events-none opacity-50"
|
||||||
<h2 className="text-3xl font-bold text-foreground">We Come to You</h2>
|
style={{
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
background: "radial-gradient(circle, oklch(0.85 0.08 200 / 0.4) 0%, transparent 70%)",
|
||||||
Living in Japan without strong Japanese skills can make tech problems frustrating.
|
}}
|
||||||
That's where we come in. Our English-speaking technicians visit your home or office
|
/>
|
||||||
to help with setup, troubleshooting, and configuration.
|
<div
|
||||||
|
className="absolute -bottom-32 -left-32 w-80 h-80 rounded-full pointer-events-none opacity-40"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(circle, oklch(0.85 0.06 80 / 0.3) 0%, transparent 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Eyebrow badge */}
|
||||||
|
<div className="flex justify-center mb-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-full bg-white/80 backdrop-blur-sm border border-primary/20 px-4 py-2 text-sm text-primary font-medium shadow-sm">
|
||||||
|
<UserCheck className="h-4 w-4" />
|
||||||
|
English-Speaking Technicians
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main heading */}
|
||||||
|
<h1
|
||||||
|
className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-center text-foreground mb-6 tracking-tight animate-in fade-in slide-in-from-bottom-4 duration-500"
|
||||||
|
style={{ animationDelay: "100ms" }}
|
||||||
|
>
|
||||||
|
Tech Help in English,
|
||||||
|
<br />
|
||||||
|
<span className="text-primary">At Your Door</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p
|
||||||
|
className="text-lg sm:text-xl text-muted-foreground text-center max-w-2xl mx-auto mb-10 leading-relaxed animate-in fade-in slide-in-from-bottom-4 duration-500"
|
||||||
|
style={{ animationDelay: "200ms" }}
|
||||||
|
>
|
||||||
|
Need help with your router, computer, or home network? Our technicians visit your home
|
||||||
|
or office to solve tech problems — explained in a language you understand.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
|
||||||
For quick fixes, we also offer remote support. We connect to your device securely over
|
{/* What we help with pills */}
|
||||||
the internet to diagnose and resolve issues without a home visit.
|
<div
|
||||||
</p>
|
className="flex flex-wrap justify-center gap-3 mb-10 animate-in fade-in slide-in-from-bottom-4 duration-500"
|
||||||
<div className="pt-4">
|
style={{ animationDelay: "300ms" }}
|
||||||
<Button as="a" href="/contact" size="lg">
|
>
|
||||||
Request Support
|
{helpItems.map(item => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.label}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/70 backdrop-blur-sm border border-border/40 shadow-sm"
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium text-foreground">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA button */}
|
||||||
|
<div
|
||||||
|
className="flex justify-center animate-in fade-in slide-in-from-bottom-4 duration-500"
|
||||||
|
style={{ animationDelay: "400ms" }}
|
||||||
|
>
|
||||||
|
<Button as="a" href="/contact" size="lg" rightIcon={<ArrowRight className="h-4 w-4" />}>
|
||||||
|
Get in Touch
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-[300px] bg-muted/30 rounded-2xl overflow-hidden flex items-center justify-center">
|
|
||||||
<Users className="h-32 w-32 text-muted-foreground/20" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pricing Cards */}
|
{/* Gradient fade to next section */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-20">
|
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-white pointer-events-none" />
|
||||||
{/* Onsite Network & Computer Support */}
|
</section>
|
||||||
<div className="bg-card rounded-2xl border border-border/60 p-6 shadow-sm flex flex-col">
|
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-4">
|
{/* How It Works Section */}
|
||||||
<Monitor className="h-6 w-6" />
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-16">
|
||||||
</div>
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<h3 className="text-lg font-bold text-foreground mb-2">
|
<h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
|
||||||
Onsite Network & Computer Support
|
How It Works
|
||||||
</h3>
|
</h2>
|
||||||
<div className="mt-auto pt-4">
|
<p className="text-muted-foreground text-center max-w-xl mx-auto mb-12">
|
||||||
<div className="text-sm text-muted-foreground mb-1">Basic Service Fee</div>
|
Getting tech help shouldn't be complicated. Here's our simple process.
|
||||||
<div className="text-3xl font-bold text-foreground">15,000 JPY</div>
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative">
|
||||||
|
{/* Connecting line (desktop only) */}
|
||||||
|
<div className="hidden md:block absolute top-16 left-[20%] right-[20%] h-0.5 bg-border" />
|
||||||
|
|
||||||
|
{steps.map((step, index) => {
|
||||||
|
const Icon = step.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={step.number}
|
||||||
|
className="relative text-center animate-in fade-in slide-in-from-bottom-8 duration-700"
|
||||||
|
style={{ animationDelay: `${index * 150}ms` }}
|
||||||
|
>
|
||||||
|
{/* Step number circle */}
|
||||||
|
<div className="relative inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary text-xl font-bold mb-5">
|
||||||
|
<Icon className="h-7 w-7" />
|
||||||
|
<span className="absolute -top-1 -right-1 w-6 h-6 rounded-full bg-white text-primary text-xs font-bold flex items-center justify-center shadow-sm border border-border">
|
||||||
|
{step.number}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-bold text-foreground mb-2">{step.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed max-w-xs mx-auto">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Remote Support */}
|
{/* Gradient fade to pricing section */}
|
||||||
<div className="bg-card rounded-2xl border border-border/60 p-6 shadow-sm flex flex-col">
|
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-[#f7f7f7] pointer-events-none" />
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-4">
|
</section>
|
||||||
<Headset className="h-6 w-6" />
|
|
||||||
</div>
|
{/* Pricing Cards Section */}
|
||||||
<h3 className="text-lg font-bold text-foreground mb-2">
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-16">
|
||||||
Remote Network & Computer Support
|
{/* Subtle pattern overlay */}
|
||||||
</h3>
|
<div
|
||||||
<div className="mt-auto pt-4">
|
className="absolute inset-0 pointer-events-none opacity-30"
|
||||||
<div className="text-sm text-muted-foreground mb-1">Basic Service Fee</div>
|
style={{
|
||||||
<div className="text-3xl font-bold text-foreground">5,000 JPY</div>
|
backgroundImage: `radial-gradient(circle at center, oklch(0.6 0.0 0 / 0.08) 1px, transparent 1px)`,
|
||||||
|
backgroundSize: "32px 32px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
|
||||||
|
Support Options
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground text-center max-w-xl mx-auto mb-12">
|
||||||
|
Choose the support type that fits your needs. All services include full English
|
||||||
|
communication.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{services.map((service, index) => {
|
||||||
|
const Icon = service.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={service.id}
|
||||||
|
className="group relative overflow-hidden rounded-2xl bg-white border border-border/60 shadow-md p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg animate-in fade-in slide-in-from-bottom-8 duration-700 flex flex-col"
|
||||||
|
style={{ animationDelay: `${index * 100}ms` }}
|
||||||
|
>
|
||||||
|
{/* Popular badge */}
|
||||||
|
{service.popular && (
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<span className="inline-flex items-center rounded-full bg-primary px-3 py-1 text-xs font-bold text-white uppercase tracking-wide shadow-sm">
|
||||||
|
Most Popular
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="w-14 h-14 rounded-2xl bg-primary/10 flex items-center justify-center mb-5 transition-transform duration-300 group-hover:scale-105">
|
||||||
|
<Icon className="h-7 w-7 text-primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtitle */}
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1">
|
||||||
|
{service.subtitle}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h3 className="text-xl font-bold text-foreground mb-2">{service.title}</h3>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed mb-4">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<div className="mb-5">
|
||||||
|
<span className="text-3xl font-bold text-foreground">{service.price}</span>
|
||||||
|
<span className="text-sm text-muted-foreground ml-1">JPY</span>
|
||||||
|
<span className="text-xs text-muted-foreground block mt-0.5">
|
||||||
|
Basic service fee
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<div className="space-y-2 mb-6 flex-grow">
|
||||||
|
{service.features.map(feature => (
|
||||||
|
<div
|
||||||
|
key={feature}
|
||||||
|
className="flex items-center gap-2 text-sm text-foreground"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-primary flex-shrink-0" />
|
||||||
|
<span>{feature}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<Link
|
||||||
|
href="/contact"
|
||||||
|
className={`inline-flex items-center justify-center gap-2 rounded-full ${service.popular ? "bg-primary text-white hover:bg-primary/90" : "bg-muted text-foreground hover:bg-muted/80"} px-5 py-2.5 font-semibold transition-colors text-sm`}
|
||||||
|
>
|
||||||
|
Request This Service
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Onsite TV Support */}
|
{/* Gradient fade to FAQ section */}
|
||||||
<div className="bg-card rounded-2xl border border-border/60 p-6 shadow-sm flex flex-col">
|
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-white pointer-events-none" />
|
||||||
<div className="h-12 w-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-4">
|
</section>
|
||||||
<Tv className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-bold text-foreground mb-2">Onsite TV Support Service</h3>
|
|
||||||
<div className="mt-auto pt-4">
|
|
||||||
<div className="text-sm text-muted-foreground mb-1">Basic Service Fee</div>
|
|
||||||
<div className="text-3xl font-bold text-foreground">15,000 JPY</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
<div className="mb-12">
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-16">
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-6 text-center">
|
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
Frequently Asked Questions
|
<h2 className="text-2xl sm:text-3xl font-bold text-foreground text-center mb-4">
|
||||||
</h2>
|
Frequently Asked Questions
|
||||||
<div className="space-y-4 max-w-3xl mx-auto">
|
</h2>
|
||||||
<FaqItem
|
<p className="text-muted-foreground text-center max-w-xl mx-auto mb-10">
|
||||||
question="My home requires multiple Wi-Fi routers. Would you be able to assist with this?"
|
Common questions about our onsite support services.
|
||||||
answer={
|
</p>
|
||||||
<>
|
|
||||||
Yes, the Assist Solutions technical team is able to visit your residence for device
|
|
||||||
set up including Wi-Fi routers, printers, Apple TVs etc.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Our tech consulting team will be able to make suggestions based on your residence
|
|
||||||
layout and requirements. Please contact us at info@asolutions.co.jp for a free
|
|
||||||
consultation.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FaqItem
|
|
||||||
question="I am already subscribed to a different Internet provider but require more Wi-Fi coverage. Would I be able to just opt for the Onsite Support service without switching over my entire home Internet service?"
|
|
||||||
answer="Yes, we are able to offer the Onsite Support service as a standalone service."
|
|
||||||
/>
|
|
||||||
<FaqItem
|
|
||||||
question="Do you offer this service outside of Tokyo?"
|
|
||||||
answer={
|
|
||||||
<>
|
|
||||||
Our In-Home Technical Assistance service can be provided in Tokyo, Saitama and
|
|
||||||
Kanagawa prefecture.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
*Please note that this service may not available in some areas within the above
|
|
||||||
prefectures.
|
|
||||||
<br />
|
|
||||||
For more information, please contact us at info@asolutions.co.jp
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA */}
|
<div className="space-y-4">
|
||||||
<div className="text-center py-12 bg-muted/20 rounded-3xl">
|
<FaqItem
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-4">
|
question="My home requires multiple Wi-Fi routers. Would you be able to assist with this?"
|
||||||
Tech Problems? We Speak Your Language.
|
answer={
|
||||||
</h2>
|
<>
|
||||||
<p className="text-muted-foreground mb-6 max-w-xl mx-auto">
|
Yes, our technical team can visit your residence for device setup including Wi-Fi
|
||||||
Don't struggle with Japanese-only support lines. Get help from technicians who
|
routers, mesh systems, printers, Apple TVs, and more.
|
||||||
explain things clearly in English.
|
<br />
|
||||||
</p>
|
<br />
|
||||||
<Button as="a" href="/contact" size="lg">
|
We'll assess your residence layout and recommend the best solution for
|
||||||
Request Support
|
complete coverage. Contact us at info@asolutions.co.jp for a free consultation.
|
||||||
</Button>
|
</>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
<FaqItem
|
||||||
|
question="I use a different Internet provider. Can I still use your onsite support?"
|
||||||
|
answer="Absolutely. Our onsite support is available as a standalone service regardless of your internet provider. We help with any networking, computer, or device issues."
|
||||||
|
/>
|
||||||
|
<FaqItem
|
||||||
|
question="How quickly can you come to my location?"
|
||||||
|
answer={
|
||||||
|
<>
|
||||||
|
We typically schedule visits within the same week. For urgent issues, we do our
|
||||||
|
best to accommodate faster appointments based on availability.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Remote support is often available within 1-2 business days if you need faster
|
||||||
|
help.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FaqItem
|
||||||
|
question="What if the problem can't be fixed in one visit?"
|
||||||
|
answer="If additional work is needed, we'll explain what's required and provide a clear quote for any follow-up visits. We don't charge extra for the same issue if it takes more time than expected during a single visit."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gradient fade to CTA section */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-[#f7f7f7] pointer-events-none" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] py-16">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2 text-sm text-primary font-medium mb-6">
|
||||||
|
<Shield className="h-4 w-4" />
|
||||||
|
Trusted by expats across Japan
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-foreground mb-4">
|
||||||
|
Tech Problems? We Speak Your Language.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mb-8 max-w-lg mx-auto leading-relaxed">
|
||||||
|
Don't struggle with Japanese-only support lines. Get help from technicians who
|
||||||
|
explain things clearly in English.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button as="a" href="/contact" size="lg" rightIcon={<ArrowRight className="h-4 w-4" />}>
|
||||||
|
Get in Touch
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trust indicators */}
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-6 mt-10 pt-8 border-t border-border/60">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Clock className="h-4 w-4 text-primary" />
|
||||||
|
Same-week appointments
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<UserCheck className="h-4 w-4 text-primary" />
|
||||||
|
English-speaking technicians
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Shield className="h-4 w-4 text-primary" />
|
||||||
|
20+ years in Japan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -152,20 +433,30 @@ function FaqItem({ question, answer }: { question: string; answer: React.ReactNo
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-border rounded-xl overflow-hidden bg-card">
|
<div
|
||||||
|
className={`border rounded-xl overflow-hidden bg-white shadow-sm transition-all duration-200 ${isOpen ? "border-primary/30 shadow-md" : "border-border/60"}`}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="w-full flex items-start justify-between gap-3 p-4 text-left hover:bg-muted/50 transition-colors"
|
className="w-full flex items-start justify-between gap-4 p-5 text-left hover:bg-muted/30 transition-colors"
|
||||||
>
|
>
|
||||||
<span className="font-medium text-foreground">{question}</span>
|
<span className="font-semibold text-foreground">{question}</span>
|
||||||
<ChevronDown
|
<div
|
||||||
className={`w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
|
className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 transition-colors ${isOpen ? "bg-primary text-white" : "bg-muted text-muted-foreground"}`}
|
||||||
/>
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
<div
|
||||||
<div className="px-4 pb-4 text-sm text-muted-foreground leading-relaxed">{answer}</div>
|
className={`overflow-hidden transition-all duration-200 ${isOpen ? "max-h-96" : "max-h-0"}`}
|
||||||
)}
|
>
|
||||||
|
<div className="px-5 pb-5 text-sm text-muted-foreground leading-relaxed border-t border-border/40 pt-4">
|
||||||
|
{answer}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
Wifi,
|
Wifi,
|
||||||
@ -10,10 +12,9 @@ import {
|
|||||||
Headphones,
|
Headphones,
|
||||||
Building2,
|
Building2,
|
||||||
Wrench,
|
Wrench,
|
||||||
Tv,
|
Zap,
|
||||||
|
Check,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { ServiceCard } from "@/components/molecules/ServiceCard";
|
|
||||||
import { ServicesHero } from "@/features/services/components/base/ServicesHero";
|
|
||||||
|
|
||||||
interface ServicesOverviewContentProps {
|
interface ServicesOverviewContentProps {
|
||||||
/** Base path for service links ("/services" or "/account/services") */
|
/** Base path for service links ("/services" or "/account/services") */
|
||||||
@ -24,11 +25,74 @@ interface ServicesOverviewContentProps {
|
|||||||
showCta?: boolean;
|
showCta?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service data with enhanced information
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
id: "internet",
|
||||||
|
icon: Wifi,
|
||||||
|
title: "Internet",
|
||||||
|
subtitle: "Fiber Optic",
|
||||||
|
description:
|
||||||
|
"NTT Optical Fiber for homes and apartments. Speeds up to 10Gbps with professional installation.",
|
||||||
|
price: "¥3,200",
|
||||||
|
priceUnit: "/mo",
|
||||||
|
features: ["Up to 10Gbps", "NTT Network", "Pro Install"],
|
||||||
|
useBasePath: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sim",
|
||||||
|
icon: Smartphone,
|
||||||
|
title: "SIM & eSIM",
|
||||||
|
subtitle: "Mobile Data",
|
||||||
|
description:
|
||||||
|
"Data, voice & SMS on NTT Docomo network. Physical SIM or instant eSIM activation.",
|
||||||
|
price: "¥1,100",
|
||||||
|
priceUnit: "/mo",
|
||||||
|
badge: "1st month free",
|
||||||
|
features: ["Docomo Network", "Voice + Data", "eSIM Ready"],
|
||||||
|
useBasePath: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "vpn",
|
||||||
|
icon: ShieldCheck,
|
||||||
|
title: "VPN Router",
|
||||||
|
subtitle: "Streaming Access",
|
||||||
|
description:
|
||||||
|
"Access US & UK streaming content with a pre-configured router. Simple plug-and-play.",
|
||||||
|
price: "¥2,500",
|
||||||
|
priceUnit: "/mo",
|
||||||
|
features: ["US/UK Content", "Pre-configured", "Plug & Play"],
|
||||||
|
useBasePath: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "business",
|
||||||
|
icon: Building2,
|
||||||
|
title: "Business",
|
||||||
|
subtitle: "Enterprise IT",
|
||||||
|
description:
|
||||||
|
"Enterprise solutions for offices and commercial spaces. Dedicated support and SLAs.",
|
||||||
|
features: ["Office Setup", "Dedicated Support", "Custom SLAs"],
|
||||||
|
useBasePath: false,
|
||||||
|
fixedPath: "/services/business",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "onsite",
|
||||||
|
icon: Wrench,
|
||||||
|
title: "Onsite Support",
|
||||||
|
subtitle: "Tech Assistance",
|
||||||
|
description:
|
||||||
|
"Professional technicians visit your location for setup, troubleshooting, and maintenance.",
|
||||||
|
features: ["Home Visits", "Setup Help", "Troubleshooting"],
|
||||||
|
useBasePath: false,
|
||||||
|
fixedPath: "/services/onsite",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServicesOverviewContent - Shared content component for services overview pages.
|
* ServicesOverviewContent - Enhanced services overview with rich visual design.
|
||||||
*
|
*
|
||||||
* Used by both PublicServicesOverview and AccountServicesOverview to ensure
|
* Features full-width sections, gradient backgrounds, and polished card treatments
|
||||||
* consistent design across public and authenticated service listing pages.
|
* matching the website's landing page aesthetic.
|
||||||
*/
|
*/
|
||||||
export function ServicesOverviewContent({
|
export function ServicesOverviewContent({
|
||||||
basePath,
|
basePath,
|
||||||
@ -36,128 +100,258 @@ export function ServicesOverviewContent({
|
|||||||
showCta = true,
|
showCta = true,
|
||||||
}: ServicesOverviewContentProps) {
|
}: ServicesOverviewContentProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-12 pb-16">
|
<div className="relative">
|
||||||
{/* Hero */}
|
{/* Hero Section with gradient background */}
|
||||||
{showHero && (
|
{showHero && (
|
||||||
<>
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 overflow-hidden bg-gradient-to-br from-slate-50 via-white to-sky-50/80 pt-8 pb-16">
|
||||||
<ServicesHero
|
{/* Dot grid pattern */}
|
||||||
title="Our Services"
|
<div
|
||||||
description="Connectivity and support solutions for Japan's international community."
|
className="absolute inset-0 pointer-events-none"
|
||||||
eyebrow={
|
style={{
|
||||||
<span className="inline-flex items-center gap-2 rounded-full bg-primary/8 border border-primary/15 px-4 py-2 text-sm text-primary font-medium normal-case tracking-normal">
|
backgroundImage: `radial-gradient(circle at center, oklch(0.65 0.05 234.4 / 0.12) 1px, transparent 1px)`,
|
||||||
|
backgroundSize: "24px 24px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Gradient orb accent */}
|
||||||
|
<div
|
||||||
|
className="absolute -top-32 -right-32 w-96 h-96 rounded-full pointer-events-none opacity-60"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(circle, oklch(0.85 0.08 200 / 0.4) 0%, transparent 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Eyebrow badge */}
|
||||||
|
<div className="flex justify-center mb-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-full bg-white/80 backdrop-blur-sm border border-primary/20 px-4 py-2 text-sm text-primary font-medium shadow-sm">
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
Full English Support
|
Full English Support
|
||||||
</span>
|
</span>
|
||||||
}
|
</div>
|
||||||
animated
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Value Props - Compact */}
|
{/* Main heading */}
|
||||||
<section
|
<h1
|
||||||
className="flex flex-wrap justify-center gap-6 text-sm animate-in fade-in slide-in-from-bottom-8 duration-700"
|
className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-center text-foreground mb-6 tracking-tight animate-in fade-in slide-in-from-bottom-4 duration-500"
|
||||||
style={{ animationDelay: "300ms" }}
|
style={{ animationDelay: "100ms" }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
Our Services
|
||||||
<Globe className="h-4 w-4 text-primary" />
|
</h1>
|
||||||
<span>One provider, all services</span>
|
|
||||||
|
{/* Description */}
|
||||||
|
<p
|
||||||
|
className="text-lg sm:text-xl text-muted-foreground text-center max-w-2xl mx-auto mb-10 leading-relaxed animate-in fade-in slide-in-from-bottom-4 duration-500"
|
||||||
|
style={{ animationDelay: "200ms" }}
|
||||||
|
>
|
||||||
|
Connectivity and support solutions designed for Japan's international community.
|
||||||
|
One provider for all your needs.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Value propositions */}
|
||||||
|
<div
|
||||||
|
className="flex flex-wrap justify-center gap-4 sm:gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500"
|
||||||
|
style={{ animationDelay: "300ms" }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-border/40">
|
||||||
|
<Globe className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium text-foreground">
|
||||||
|
One provider, all services
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-border/40">
|
||||||
|
<Headphones className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium text-foreground">English support</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 backdrop-blur-sm border border-border/40">
|
||||||
|
<Zap className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium text-foreground">Fast activation</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
</div>
|
||||||
<Headphones className="h-4 w-4 text-success" />
|
|
||||||
<span>English support</span>
|
{/* Gradient fade to services section */}
|
||||||
</div>
|
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-[#f7f7f7] pointer-events-none" />
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
</section>
|
||||||
<CheckCircle2 className="h-4 w-4 text-info" />
|
|
||||||
<span>No hidden fees</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* All Services - Clean Grid with staggered animations */}
|
{/* Services Section */}
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 cp-stagger-children">
|
<section
|
||||||
<ServiceCard
|
className={`relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-[#f7f7f7] ${showHero ? "pt-8" : "pt-12"} pb-16`}
|
||||||
href={`${basePath}/internet`}
|
>
|
||||||
icon={<Wifi className="h-6 w-6" />}
|
{/* Subtle pattern overlay */}
|
||||||
title="Internet"
|
<div
|
||||||
description="NTT Optical Fiber for homes and apartments. Speeds up to 10Gbps with professional installation."
|
className="absolute inset-0 pointer-events-none opacity-30"
|
||||||
price="¥3,200/mo"
|
style={{
|
||||||
accentColor="blue"
|
backgroundImage: `radial-gradient(circle at center, oklch(0.6 0.0 0 / 0.08) 1px, transparent 1px)`,
|
||||||
|
backgroundSize: "32px 32px",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ServiceCard
|
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
href={`${basePath}/sim`}
|
{/* Section header */}
|
||||||
icon={<Smartphone className="h-6 w-6" />}
|
<div className="flex items-center justify-between mb-8">
|
||||||
title="SIM & eSIM"
|
<h2 className="text-2xl sm:text-3xl font-bold text-foreground">Choose Your Service</h2>
|
||||||
description="Data, voice & SMS on NTT Docomo network. Physical SIM or instant eSIM activation."
|
<div className="hidden sm:flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
price="¥1,100/mo"
|
<CheckCircle2 className="h-4 w-4 text-primary" />
|
||||||
badge="1st month free"
|
<span>No hidden fees</span>
|
||||||
accentColor="green"
|
</div>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<ServiceCard
|
{/* Featured services - Top row (Internet & SIM) */}
|
||||||
href={`${basePath}/vpn`}
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
icon={<ShieldCheck className="h-6 w-6" />}
|
{services.slice(0, 2).map((service, index) => {
|
||||||
title="VPN Router"
|
const Icon = service.icon;
|
||||||
description="Access US & UK streaming content with a pre-configured router. Simple plug-and-play."
|
const href = service.useBasePath ? `${basePath}/${service.id}` : service.fixedPath;
|
||||||
price="¥2,500/mo"
|
|
||||||
accentColor="purple"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Business, Onsite, and TV services only have public pages (no account-specific routes) */}
|
return (
|
||||||
<ServiceCard
|
<Link
|
||||||
href="/services/business"
|
key={service.id}
|
||||||
icon={<Building2 className="h-6 w-6" />}
|
href={href!}
|
||||||
title="Business"
|
className="group relative overflow-hidden rounded-2xl bg-white border border-border/60 shadow-md p-6 sm:p-8 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg animate-in fade-in slide-in-from-bottom-8 duration-700"
|
||||||
description="Enterprise solutions for offices and commercial spaces. Dedicated support and SLAs."
|
style={{ animationDelay: `${index * 100}ms` }}
|
||||||
accentColor="orange"
|
>
|
||||||
/>
|
{/* Badge */}
|
||||||
|
{service.badge && (
|
||||||
|
<div className="absolute top-4 right-4 sm:top-6 sm:right-6">
|
||||||
|
<span className="inline-flex items-center rounded-full bg-primary px-3 py-1 text-xs font-bold text-white uppercase tracking-wide shadow-sm">
|
||||||
|
{service.badge}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ServiceCard
|
{/* Icon */}
|
||||||
href="/services/onsite"
|
<div className="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl bg-primary/10 flex items-center justify-center mb-5 transition-transform duration-300 group-hover:scale-105">
|
||||||
icon={<Wrench className="h-6 w-6" />}
|
<Icon className="h-7 w-7 sm:h-8 sm:w-8 text-primary" />
|
||||||
title="Onsite Support"
|
</div>
|
||||||
description="Professional technicians visit your location for setup, troubleshooting, and maintenance."
|
|
||||||
accentColor="cyan"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ServiceCard
|
{/* Subtitle */}
|
||||||
href="/services/tv"
|
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1 block">
|
||||||
icon={<Tv className="h-6 w-6" />}
|
{service.subtitle}
|
||||||
title="TV"
|
</span>
|
||||||
description="Streaming TV packages with international channels. Watch content from home countries."
|
|
||||||
accentColor="pink"
|
{/* Title */}
|
||||||
/>
|
<h3 className="text-xl sm:text-2xl font-bold text-foreground mb-3">
|
||||||
|
{service.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-muted-foreground leading-relaxed mb-5">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<div className="flex flex-wrap gap-2 mb-5">
|
||||||
|
{service.features.map(feature => (
|
||||||
|
<span
|
||||||
|
key={feature}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1.5 text-xs font-medium text-foreground"
|
||||||
|
>
|
||||||
|
<Check className="h-3 w-3 text-primary" />
|
||||||
|
{feature}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<div className="flex items-center gap-2 text-primary font-semibold group-hover:gap-3 transition-all duration-300">
|
||||||
|
<span>View Plans</span>
|
||||||
|
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Secondary services - Bottom row */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||||
|
{services.slice(2).map((service, index) => {
|
||||||
|
const Icon = service.icon;
|
||||||
|
const href = service.useBasePath ? `${basePath}/${service.id}` : service.fixedPath;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={service.id}
|
||||||
|
href={href!}
|
||||||
|
className="group relative overflow-hidden rounded-2xl bg-white border border-border/60 shadow-sm p-5 sm:p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-md animate-in fade-in slide-in-from-bottom-8 duration-700"
|
||||||
|
style={{ animationDelay: `${(index + 2) * 100}ms` }}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center flex-shrink-0 transition-transform duration-300 group-hover:scale-105">
|
||||||
|
<Icon className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{/* Subtitle */}
|
||||||
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-0.5 block">
|
||||||
|
{service.subtitle}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h3 className="text-lg font-bold text-foreground mb-2">{service.title}</h3>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed mb-3 line-clamp-2">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Features as pills */}
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{service.features.slice(0, 2).map(feature => (
|
||||||
|
<span
|
||||||
|
key={feature}
|
||||||
|
className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-foreground"
|
||||||
|
>
|
||||||
|
{feature}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover arrow indicator */}
|
||||||
|
<div className="absolute bottom-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
|
<ArrowRight className="h-4 w-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gradient fade to CTA section */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-white pointer-events-none" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* CTA Section */}
|
||||||
{showCta && (
|
{showCta && (
|
||||||
<section
|
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-12 sm:py-16">
|
||||||
className="rounded-2xl bg-gradient-to-br from-muted/50 to-muted/80 p-8 text-center animate-in fade-in slide-in-from-bottom-8 duration-700"
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
style={{ animationDelay: "500ms" }}
|
<h2 className="text-2xl sm:text-3xl font-bold text-foreground mb-4">
|
||||||
>
|
Need help choosing?
|
||||||
<h2 className="text-xl font-bold text-foreground font-display mb-3">
|
</h2>
|
||||||
Need help choosing?
|
<p className="text-muted-foreground mb-8 max-w-lg mx-auto leading-relaxed">
|
||||||
</h2>
|
Our bilingual team is ready to help you find the perfect solution for your needs. Get
|
||||||
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
|
personalized recommendations in English.
|
||||||
Our bilingual team can help you find the right solution.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
<Link
|
<Link
|
||||||
href="/contact"
|
href="/contact"
|
||||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-5 py-2.5 font-medium text-primary-foreground hover:bg-primary-hover transition-colors"
|
className="inline-flex items-center gap-2 rounded-full bg-primary px-6 py-3 font-semibold text-white hover:bg-primary/90 transition-colors shadow-md"
|
||||||
>
|
>
|
||||||
Contact Us
|
Get in Touch
|
||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
href="tel:0120660470"
|
href="tel:0120660470"
|
||||||
className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||||
aria-label="Call us toll free at 0120-660-470"
|
aria-label="Call us toll free at 0120-660-470"
|
||||||
>
|
>
|
||||||
<Phone className="h-4 w-4" aria-hidden="true" />
|
<Phone className="h-4 w-4" aria-hidden="true" />
|
||||||
0120-660-470 (Toll Free)
|
0120-660-470 (Toll Free)
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -408,7 +408,7 @@ export function CancellationFlow({
|
|||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<div className="fixed inset-0 bg-background/70 backdrop-blur-sm transition-opacity"></div>
|
<div className="fixed inset-0 bg-background/70 backdrop-blur-sm transition-opacity"></div>
|
||||||
<div className="inline-block align-bottom bg-card rounded-lg text-left overflow-hidden shadow-[var(--cp-shadow-3)] border border-border transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
<div className="relative z-10 inline-block align-bottom bg-card rounded-lg text-left overflow-hidden shadow-[var(--cp-shadow-3)] border border-border transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
<div className="bg-card px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="bg-card px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-danger-soft sm:mx-0 sm:h-10 sm:w-10">
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-danger-soft sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export function ChangePlanModal({
|
|||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
​
|
​
|
||||||
</span>
|
</span>
|
||||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
<div className="relative z-10 inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
|
||||||
|
|||||||
@ -217,7 +217,7 @@ function CancelConfirmModal({
|
|||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<div className="fixed inset-0 bg-background/70 backdrop-blur-sm transition-opacity"></div>
|
<div className="fixed inset-0 bg-background/70 backdrop-blur-sm transition-opacity"></div>
|
||||||
<div className="inline-block align-bottom bg-card rounded-lg text-left overflow-hidden shadow-[var(--cp-shadow-3)] border border-border transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
<div className="relative z-10 inline-block align-bottom bg-card rounded-lg text-left overflow-hidden shadow-[var(--cp-shadow-3)] border border-border transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
<div className="bg-card px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="bg-card px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-danger-soft sm:mx-0 sm:h-10 sm:w-10">
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-danger-soft sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
|||||||
@ -319,40 +319,48 @@ export function SimDetailsCard({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
{simDetails.hasVoice === false ? (
|
||||||
<div className="flex items-center">
|
<p className="text-sm text-muted-foreground">
|
||||||
<SignalIcon
|
Data-only plan (no voice features)
|
||||||
className={`h-4 w-4 mr-1 ${simDetails.voiceMailEnabled ? "text-success" : "text-muted-foreground"}`}
|
</p>
|
||||||
/>
|
) : (
|
||||||
<span
|
<>
|
||||||
className={`text-sm ${simDetails.voiceMailEnabled ? "text-success" : "text-muted-foreground"}`}
|
<div className="flex items-center space-x-4">
|
||||||
>
|
<div className="flex items-center">
|
||||||
Voicemail {simDetails.voiceMailEnabled ? "Enabled" : "Disabled"}
|
<SignalIcon
|
||||||
</span>
|
className={`h-4 w-4 mr-1 ${simDetails.voiceMailEnabled ? "text-success" : "text-muted-foreground"}`}
|
||||||
</div>
|
/>
|
||||||
<div className="flex items-center">
|
<span
|
||||||
<DevicePhoneMobileIcon
|
className={`text-sm ${simDetails.voiceMailEnabled ? "text-success" : "text-muted-foreground"}`}
|
||||||
className={`h-4 w-4 mr-1 ${simDetails.callWaitingEnabled ? "text-success" : "text-muted-foreground"}`}
|
>
|
||||||
/>
|
Voicemail {simDetails.voiceMailEnabled ? "Enabled" : "Disabled"}
|
||||||
<span
|
</span>
|
||||||
className={`text-sm ${simDetails.callWaitingEnabled ? "text-success" : "text-muted-foreground"}`}
|
</div>
|
||||||
>
|
<div className="flex items-center">
|
||||||
Call Waiting {simDetails.callWaitingEnabled ? "Enabled" : "Disabled"}
|
<DevicePhoneMobileIcon
|
||||||
</span>
|
className={`h-4 w-4 mr-1 ${simDetails.callWaitingEnabled ? "text-success" : "text-muted-foreground"}`}
|
||||||
</div>
|
/>
|
||||||
</div>
|
<span
|
||||||
|
className={`text-sm ${simDetails.callWaitingEnabled ? "text-success" : "text-muted-foreground"}`}
|
||||||
|
>
|
||||||
|
Call Waiting {simDetails.callWaitingEnabled ? "Enabled" : "Disabled"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<WifiIcon
|
<WifiIcon
|
||||||
className={`h-4 w-4 mr-1 ${simDetails.internationalRoamingEnabled ? "text-success" : "text-muted-foreground"}`}
|
className={`h-4 w-4 mr-1 ${simDetails.internationalRoamingEnabled ? "text-success" : "text-muted-foreground"}`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`text-sm ${simDetails.internationalRoamingEnabled ? "text-success" : "text-muted-foreground"}`}
|
className={`text-sm ${simDetails.internationalRoamingEnabled ? "text-success" : "text-muted-foreground"}`}
|
||||||
>
|
>
|
||||||
Int'l Roaming{" "}
|
Int'l Roaming{" "}
|
||||||
{simDetails.internationalRoamingEnabled ? "Enabled" : "Disabled"}
|
{simDetails.internationalRoamingEnabled ? "Enabled" : "Disabled"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -37,6 +37,8 @@ interface SimDetails {
|
|||||||
callWaitingEnabled?: boolean;
|
callWaitingEnabled?: boolean;
|
||||||
internationalRoamingEnabled?: boolean;
|
internationalRoamingEnabled?: boolean;
|
||||||
networkType?: string;
|
networkType?: string;
|
||||||
|
hasVoice?: boolean;
|
||||||
|
hasSms?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SimInfo {
|
interface SimInfo {
|
||||||
@ -304,22 +306,26 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Voice toggles */}
|
{/* Voice & Network toggles */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-md font-semibold text-foreground">Voice Status</h4>
|
<h4 className="text-md font-semibold text-foreground">
|
||||||
|
{simInfo.details.hasVoice === false ? "Network Status" : "Voice Status"}
|
||||||
|
</h4>
|
||||||
{featureError && (
|
{featureError && (
|
||||||
<div className="p-3 bg-danger-soft border border-danger/25 rounded-lg text-sm text-danger">
|
<div className="p-3 bg-danger-soft border border-danger/25 rounded-lg text-sm text-danger">
|
||||||
{featureError}
|
{featureError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<StatusToggle
|
{simInfo.details.hasVoice !== false && (
|
||||||
label="Voice Mail"
|
<StatusToggle
|
||||||
subtitle={simInfo.details.voiceMailEnabled ? "Enabled" : "Disabled"}
|
label="Voice Mail"
|
||||||
checked={simInfo.details.voiceMailEnabled || false}
|
subtitle={simInfo.details.voiceMailEnabled ? "Enabled" : "Disabled"}
|
||||||
loading={featureLoading.voiceMail ?? false}
|
checked={simInfo.details.voiceMailEnabled || false}
|
||||||
onChange={checked => void updateFeature("voiceMail", checked)}
|
loading={featureLoading.voiceMail ?? false}
|
||||||
/>
|
onChange={checked => void updateFeature("voiceMail", checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<StatusToggle
|
<StatusToggle
|
||||||
label="Network Type"
|
label="Network Type"
|
||||||
subtitle={simInfo.details.networkType === "5G" ? "5G" : "4G"}
|
subtitle={simInfo.details.networkType === "5G" ? "5G" : "4G"}
|
||||||
@ -327,21 +333,30 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
|
|||||||
loading={featureLoading.networkType ?? false}
|
loading={featureLoading.networkType ?? false}
|
||||||
onChange={checked => void updateFeature("networkType", checked ? "5G" : "4G")}
|
onChange={checked => void updateFeature("networkType", checked ? "5G" : "4G")}
|
||||||
/>
|
/>
|
||||||
<StatusToggle
|
{simInfo.details.hasVoice !== false && (
|
||||||
label="Call Waiting"
|
<StatusToggle
|
||||||
subtitle={simInfo.details.callWaitingEnabled ? "Enabled" : "Disabled"}
|
label="Call Waiting"
|
||||||
checked={simInfo.details.callWaitingEnabled || false}
|
subtitle={simInfo.details.callWaitingEnabled ? "Enabled" : "Disabled"}
|
||||||
loading={featureLoading.callWaiting ?? false}
|
checked={simInfo.details.callWaitingEnabled || false}
|
||||||
onChange={checked => void updateFeature("callWaiting", checked)}
|
loading={featureLoading.callWaiting ?? false}
|
||||||
/>
|
onChange={checked => void updateFeature("callWaiting", checked)}
|
||||||
<StatusToggle
|
/>
|
||||||
label="International Roaming"
|
)}
|
||||||
subtitle={simInfo.details.internationalRoamingEnabled ? "Enabled" : "Disabled"}
|
{simInfo.details.hasVoice !== false && (
|
||||||
checked={simInfo.details.internationalRoamingEnabled || false}
|
<StatusToggle
|
||||||
loading={featureLoading.internationalRoaming ?? false}
|
label="International Roaming"
|
||||||
onChange={checked => void updateFeature("internationalRoaming", checked)}
|
subtitle={simInfo.details.internationalRoamingEnabled ? "Enabled" : "Disabled"}
|
||||||
/>
|
checked={simInfo.details.internationalRoamingEnabled || false}
|
||||||
|
loading={featureLoading.internationalRoaming ?? false}
|
||||||
|
onChange={checked => void updateFeature("internationalRoaming", checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{simInfo.details.hasVoice === false && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Voice features are not available on data-only plans.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopU
|
|||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
||||||
|
|
||||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
<div className="relative z-10 inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
|||||||
@ -61,7 +61,15 @@ export function SimChangePlanContainer() {
|
|||||||
newPlanSku: selectedPlan.sku,
|
newPlanSku: selectedPlan.sku,
|
||||||
newPlanName: selectedPlan.name,
|
newPlanName: selectedPlan.name,
|
||||||
});
|
});
|
||||||
setMessage(`Plan change scheduled for ${result.scheduledAt || "the 1st of next month"}`);
|
const scheduled = result.scheduledAt;
|
||||||
|
const today = new Date();
|
||||||
|
const todayStr = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, "0")}${String(today.getDate()).padStart(2, "0")}`;
|
||||||
|
const isImmediate = scheduled === todayStr;
|
||||||
|
setMessage(
|
||||||
|
isImmediate
|
||||||
|
? `Plan change submitted for immediate processing (${selectedPlan.name})`
|
||||||
|
: `Plan change scheduled for ${scheduled || "the 1st of next month"}`
|
||||||
|
);
|
||||||
setSelectedPlan(null);
|
setSelectedPlan(null);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setError(
|
setError(
|
||||||
@ -96,8 +104,9 @@ export function SimChangePlanContainer() {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-lg font-semibold text-foreground mb-2">Change Your Plan</h2>
|
<h2 className="text-lg font-semibold text-foreground mb-2">Change Your Plan</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Select a new plan below. Plan changes will take effect on the 1st of the following
|
{process.env.NODE_ENV === "development"
|
||||||
month. Changes must be requested before the 25th of the current month.
|
? "Select a new plan below. In test mode, plan changes are submitted for immediate processing."
|
||||||
|
: "Select a new plan below. Plan changes will take effect on the 1st of the following month. Changes must be requested before the 25th of the current month."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -205,8 +214,17 @@ export function SimChangePlanContainer() {
|
|||||||
<div className="bg-warning-soft border border-warning/25 rounded-lg p-4">
|
<div className="bg-warning-soft border border-warning/25 rounded-lg p-4">
|
||||||
<h3 className="text-sm font-medium text-foreground mb-1">Important Notes</h3>
|
<h3 className="text-sm font-medium text-foreground mb-1">Important Notes</h3>
|
||||||
<ul className="text-sm text-muted-foreground space-y-1">
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
<li>• Plan changes take effect on the 1st of the following month</li>
|
{process.env.NODE_ENV === "development" ? (
|
||||||
<li>• Requests must be made before the 25th of the current month</li>
|
<>
|
||||||
|
<li>• Test mode: plan changes are submitted for immediate processing</li>
|
||||||
|
<li>• Changes may take a few hours to appear on the Freebit dashboard</li>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<li>• Plan changes take effect on the 1st of the following month</li>
|
||||||
|
<li>• Requests must be made before the 25th of the current month</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<li>• Your current data balance will be reset when the new plan activates</li>
|
<li>• Your current data balance will be reset when the new plan activates</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user