Spaces:
Sleeping
Sleeping
| '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> | |
| ); | |
| } | |