172 lines
6.8 KiB
TypeScript
172 lines
6.8 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { XMarkIcon, PlusIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
|
import { apiClient } from "@/lib/api";
|
|
|
|
interface TopUpModalProps {
|
|
subscriptionId: number;
|
|
onClose: () => void;
|
|
onSuccess: () => void;
|
|
onError: (message: string) => void;
|
|
}
|
|
|
|
export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopUpModalProps) {
|
|
const [gbAmount, setGbAmount] = useState<string>("1");
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const getCurrentAmountMb = () => {
|
|
const gb = parseInt(gbAmount, 10);
|
|
return isNaN(gb) ? 0 : gb * 1000;
|
|
};
|
|
|
|
const isValidAmount = () => {
|
|
const gb = Number(gbAmount);
|
|
return Number.isInteger(gb) && gb >= 1 && gb <= 50; // 1-50 GB, whole numbers only (Freebit API limit)
|
|
};
|
|
|
|
const calculateCost = () => {
|
|
const gb = parseInt(gbAmount, 10);
|
|
return isNaN(gb) ? 0 : gb * 500; // 1GB = 500 JPY
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!isValidAmount()) {
|
|
onError("Please enter a whole number between 1 GB and 100 GB");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const requestBody = {
|
|
quotaMb: getCurrentAmountMb(),
|
|
};
|
|
|
|
await apiClient.POST("/api/subscriptions/{id}/sim/top-up", {
|
|
params: { path: { id: subscriptionId } },
|
|
body: requestBody,
|
|
});
|
|
|
|
onSuccess();
|
|
} catch (error: unknown) {
|
|
onError(error instanceof Error ? error.message : "Failed to top up SIM");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
if (e.target === e.currentTarget) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 overflow-y-auto" onClick={handleBackdropClick}>
|
|
<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="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">
|
|
{/* Header */}
|
|
<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">
|
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
<PlusIcon className="h-6 w-6 text-blue-600" />
|
|
</div>
|
|
<div className="ml-4">
|
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Top Up Data</h3>
|
|
<p className="text-sm text-gray-500">Add data quota to your SIM service</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-500 focus:outline-none"
|
|
>
|
|
<XMarkIcon className="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={e => void handleSubmit(e)}>
|
|
{/* Amount Input */}
|
|
<div className="mb-6">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Amount (GB)</label>
|
|
<div className="relative">
|
|
<input
|
|
type="number"
|
|
value={gbAmount}
|
|
onChange={e => setGbAmount(e.target.value)}
|
|
placeholder="Enter amount in GB"
|
|
min="1"
|
|
max="50"
|
|
step="1"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 pr-12"
|
|
/>
|
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
|
<span className="text-gray-500 text-sm">GB</span>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Enter the amount of data you want to add (1 - 50 GB, whole numbers)
|
|
</p>
|
|
</div>
|
|
|
|
{/* Cost Display */}
|
|
<div className="mb-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<div className="text-sm font-medium text-blue-900">
|
|
{gbAmount && !isNaN(parseInt(gbAmount, 10)) ? `${gbAmount} GB` : "0 GB"}
|
|
</div>
|
|
<div className="text-xs text-blue-700">= {getCurrentAmountMb()} MB</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-lg font-bold text-blue-900">
|
|
¥{calculateCost().toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-blue-700">(1GB = ¥500)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Validation Warning */}
|
|
{!isValidAmount() && gbAmount && (
|
|
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-3">
|
|
<div className="flex items-center">
|
|
<ExclamationTriangleIcon className="h-4 w-4 text-red-500 mr-2" />
|
|
<p className="text-sm text-red-800">
|
|
Amount must be a whole number between 1 GB and 50 GB
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-3 space-y-3 space-y-reverse sm:space-y-0">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={loading}
|
|
className="w-full sm:w-auto px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
>
|
|
Back
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading || !isValidAmount()}
|
|
className="w-full sm:w-auto px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
>
|
|
{loading ? "Processing..." : `Top Up Now - ¥${calculateCost().toLocaleString()}`}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|