FE_Test / components /improvement-result /url-input-connected.tsx
GitHub Actions
Deploy from GitHub Actions [test] - 2025-10-31 10:18:25
5f2aab6
'use client';
import { useCallback } from 'react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Globe, Check, AlertCircle, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useUrlInputStore } from '@/store/url-input';
import { screenshotApi } from '@/api-client/screenshot';
interface URLInputConnectedProps {
onUrlConfirm?: (url: string, screenshot?: string) => void;
disabled?: boolean;
isLoading?: boolean;
}
export function URLInputConnected({
onUrlConfirm,
disabled = false,
isLoading = false,
}: URLInputConnectedProps) {
const {
url,
isValid,
isProcessing,
screenshot,
error,
setUrl,
setScreenshot,
setProcessing,
setError,
} = useUrlInputStore();
const handleUrlChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setUrl(e.target.value);
},
[setUrl],
);
const handleConfirm = useCallback(async () => {
if (!isValid || isProcessing) return;
setProcessing(true);
setError(undefined);
try {
let processedUrl = url;
if (
!processedUrl.startsWith('http://') &&
!processedUrl.startsWith('https://')
) {
processedUrl = `https://${processedUrl}`;
}
const data = await screenshotApi.capture({
url: processedUrl,
width: 512,
height: 768,
});
if (!data.success) {
throw new Error(data.error || 'スクリーンショットの取得に失敗しました');
}
setScreenshot(data.screenshotBase64, processedUrl);
onUrlConfirm?.(processedUrl, data.screenshotBase64);
} catch (error) {
setError(error instanceof Error ? error.message : 'エラーが発生しました');
}
}, [
url,
isValid,
isProcessing,
setProcessing,
setError,
setScreenshot,
onUrlConfirm,
]);
const isButtonDisabled =
!url || !isValid || isProcessing || disabled || isLoading;
return (
<div className="flex items-start gap-2">
<div className="flex-1">
<div className="relative">
<Globe className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-500" />
<Input
type="url"
value={url}
onChange={handleUrlChange}
placeholder="参考URLを入力してください"
disabled={disabled || isLoading || isProcessing}
className={cn(
'pr-3 pl-10',
error && 'border-red-500 focus-visible:border-red-500',
)}
aria-invalid={!!error}
aria-describedby={error ? 'url-error' : undefined}
/>
{isProcessing && (
<Loader2 className="absolute top-1/2 right-3 h-4 w-4 -translate-y-1/2 animate-spin text-gray-500" />
)}
</div>
{error && (
<div
id="url-error"
className="mt-1 flex items-center gap-1 text-xs text-red-500"
>
<AlertCircle className="h-3 w-3" />
<span>{error}</span>
</div>
)}
</div>
<Button
onClick={handleConfirm}
disabled={isButtonDisabled}
size="default"
variant={screenshot ? 'secondary' : 'default'}
className="min-w-[80px]"
>
{isProcessing ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
処理中
</>
) : screenshot ? (
<>
<Check className="h-4 w-4" />
確定済
</>
) : (
'確定'
)}
</Button>
</div>
);
}