import { useCallback, useEffect, useState } from 'react' import { ApiClient } from '@/api/client' import { Spinner } from '@/components/Spinner' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import type { ServerUrlResult } from '@/hooks/useServerUrl' type LoginPromptProps = { mode?: 'login' | 'bind' onLogin?: (token: string) => void onBind?: (token: string) => Promise baseUrl: string serverUrl: string | null setServerUrl: (input: string) => ServerUrlResult clearServerUrl: () => void error?: string | null } export function LoginPrompt(props: LoginPromptProps) { const isBindMode = props.mode === 'bind' const [accessToken, setAccessToken] = useState('') const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const [isServerDialogOpen, setIsServerDialogOpen] = useState(false) const [serverInput, setServerInput] = useState(props.serverUrl ?? '') const [serverError, setServerError] = useState(null) const handleSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault() const trimmedToken = accessToken.trim() if (!trimmedToken) { setError('Please enter an access token') return } setIsLoading(true) setError(null) try { if (isBindMode) { if (!props.onBind) { setError('Binding is unavailable.') return } await props.onBind(trimmedToken) } else { // Validate the token by attempting to authenticate const client = new ApiClient('', { baseUrl: props.baseUrl }) await client.authenticate({ accessToken: trimmedToken }) // If successful, pass the token to parent if (!props.onLogin) { setError('Login is unavailable.') return } props.onLogin(trimmedToken) } } catch (e) { const fallbackMessage = isBindMode ? 'Binding failed' : 'Authentication failed' setError(e instanceof Error ? e.message : fallbackMessage) } finally { setIsLoading(false) } }, [accessToken, props]) useEffect(() => { if (!isServerDialogOpen) { return } setServerInput(props.serverUrl ?? '') setServerError(null) }, [isServerDialogOpen, props.serverUrl]) const handleSaveServer = useCallback((e: React.FormEvent) => { e.preventDefault() const result = props.setServerUrl(serverInput) if (!result.ok) { setServerError(result.error) return } setServerError(null) setServerInput(result.value) setIsServerDialogOpen(false) }, [props, serverInput]) const handleClearServer = useCallback(() => { props.clearServerUrl() setServerInput('') setServerError(null) setIsServerDialogOpen(false) }, [props]) const displayError = error || props.error const serverSummary = props.serverUrl ?? `${props.baseUrl} (same origin)` const title = isBindMode ? 'Bind Telegram' : 'HAPI' const subtitle = 'Vibe Coding Anytime, Anywhere' const submitLabel = isBindMode ? 'Bind' : 'Sign In' return (
{!isBindMode && (
Server URL Set the hapi server origin for API and live updates.
Current: {serverSummary}
{ setServerInput(e.target.value) setServerError(null) }} placeholder="https://hapi.example.com" className="w-full px-3 py-2.5 rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] text-[var(--app-fg)] placeholder:text-[var(--app-hint)] focus:outline-none focus:ring-2 focus:ring-[var(--app-button)] focus:border-transparent" />
Use http(s) only. Any path is ignored.
{serverError && (
{serverError}
)}
{props.serverUrl && ( )}
)}
{/* Header */}
{title}
{subtitle}
{/* Form */}
setAccessToken(e.target.value)} placeholder="Access token" autoComplete="current-password" disabled={isLoading} className="w-full px-3 py-2.5 rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] text-[var(--app-fg)] placeholder:text-[var(--app-hint)] focus:outline-none focus:ring-2 focus:ring-[var(--app-button)] focus:border-transparent disabled:opacity-50" />
{displayError && (
{displayError}
)}
{/* Footer */}
Designed with for Vibe Coding
© {new Date().getFullYear()} HAPI
) }