Files
PodTui/src/components/LoginScreen.tsx
2026-02-04 22:48:54 -05:00

176 lines
5.1 KiB
TypeScript

/**
* Login screen component for PodTUI
* Email/password login with links to code validation and OAuth
*/
import { createSignal } from "solid-js"
import { useAuthStore } from "../stores/auth"
import { AUTH_CONFIG } from "../config/auth"
interface LoginScreenProps {
focused?: boolean
onNavigateToCode?: () => void
onNavigateToOAuth?: () => void
}
type FocusField = "email" | "password" | "submit" | "code" | "oauth"
export function LoginScreen(props: LoginScreenProps) {
const auth = useAuthStore()
const [email, setEmail] = createSignal("")
const [password, setPassword] = createSignal("")
const [focusField, setFocusField] = createSignal<FocusField>("email")
const [emailError, setEmailError] = createSignal<string | null>(null)
const [passwordError, setPasswordError] = createSignal<string | null>(null)
const fields: FocusField[] = ["email", "password", "submit", "code", "oauth"]
const validateEmail = (value: string): boolean => {
if (!value) {
setEmailError("Email is required")
return false
}
if (!AUTH_CONFIG.email.pattern.test(value)) {
setEmailError("Invalid email format")
return false
}
setEmailError(null)
return true
}
const validatePassword = (value: string): boolean => {
if (!value) {
setPasswordError("Password is required")
return false
}
if (value.length < AUTH_CONFIG.password.minLength) {
setPasswordError(`Minimum ${AUTH_CONFIG.password.minLength} characters`)
return false
}
setPasswordError(null)
return true
}
const handleSubmit = async () => {
const isEmailValid = validateEmail(email())
const isPasswordValid = validatePassword(password())
if (!isEmailValid || !isPasswordValid) {
return
}
await auth.login({ email: email(), password: password() })
}
const handleKeyPress = (key: { name: string; shift?: boolean }) => {
if (key.name === "tab") {
const currentIndex = fields.indexOf(focusField())
const nextIndex = key.shift
? (currentIndex - 1 + fields.length) % fields.length
: (currentIndex + 1) % fields.length
setFocusField(fields[nextIndex])
} else if (key.name === "return" || key.name === "enter") {
if (focusField() === "submit") {
handleSubmit()
} else if (focusField() === "code" && props.onNavigateToCode) {
props.onNavigateToCode()
} else if (focusField() === "oauth" && props.onNavigateToOAuth) {
props.onNavigateToOAuth()
}
}
}
return (
<box flexDirection="column" border padding={2} gap={1}>
<text>
<strong>Sign In</strong>
</text>
<box height={1} />
{/* Email field */}
<box flexDirection="column" gap={0}>
<text fg={focusField() === "email" ? "var(--color-primary)" : undefined}>Email:</text>
<input
value={email()}
onInput={setEmail}
placeholder="your@email.com"
focused={props.focused && focusField() === "email"}
width={30}
/>
{emailError() && (
<text fg="var(--color-error)">{emailError()}</text>
)}
</box>
{/* Password field */}
<box flexDirection="column" gap={0}>
<text fg={focusField() === "password" ? "var(--color-primary)" : undefined}>
Password:
</text>
<input
value={password()}
onInput={setPassword}
placeholder="********"
focused={props.focused && focusField() === "password"}
width={30}
/>
{passwordError() && (
<text fg="var(--color-error)">{passwordError()}</text>
)}
</box>
<box height={1} />
{/* Submit button */}
<box flexDirection="row" gap={2}>
<box
border
padding={1}
backgroundColor={focusField() === "submit" ? "var(--color-primary)" : undefined}
>
<text fg={focusField() === "submit" ? "var(--color-text)" : undefined}>
{auth.isLoading ? "Signing in..." : "[Enter] Sign In"}
</text>
</box>
</box>
{/* Auth error message */}
{auth.error && (
<text fg="var(--color-error)">{auth.error.message}</text>
)}
<box height={1} />
{/* Alternative auth options */}
<text fg="var(--color-muted)">Or authenticate with:</text>
<box flexDirection="row" gap={2}>
<box
border
padding={1}
backgroundColor={focusField() === "code" ? "var(--color-primary)" : undefined}
>
<text fg={focusField() === "code" ? "var(--color-accent)" : "var(--color-muted)"}>
[C] Sync Code
</text>
</box>
<box
border
padding={1}
backgroundColor={focusField() === "oauth" ? "var(--color-primary)" : undefined}
>
<text fg={focusField() === "oauth" ? "var(--color-accent)" : "var(--color-muted)"}>
[O] OAuth Info
</text>
</box>
</box>
<box height={1} />
<text fg="var(--color-muted)">Tab to navigate, Enter to select</text>
</box>
)
}