2026-02-09 22:31:52 +00:00
|
|
|
import React, { useId, useState } from 'react';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { cn } from '@/lib/utils';
|
2026-02-09 22:31:52 +00:00
|
|
|
import { Eye, EyeOff } from 'lucide-react';
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
interface AuthInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
|
|
|
error?: string;
|
|
|
|
|
label?: string;
|
2026-02-09 22:31:52 +00:00
|
|
|
showPasswordToggle?: boolean;
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function AuthInput({
|
|
|
|
|
error,
|
|
|
|
|
label,
|
|
|
|
|
className,
|
|
|
|
|
id,
|
2026-02-09 22:31:52 +00:00
|
|
|
showPasswordToggle,
|
|
|
|
|
type,
|
2025-12-03 21:56:50 +00:00
|
|
|
...props
|
|
|
|
|
}: AuthInputProps) {
|
2026-01-07 09:32:53 +00:00
|
|
|
// CRITIQUE FIX #5: Utiliser useId() de React pour générer un ID stable
|
|
|
|
|
// qui ne change pas entre les renders, contrairement à Math.random()
|
|
|
|
|
const generatedId = useId();
|
|
|
|
|
const inputId = id || generatedId;
|
2026-02-09 22:31:52 +00:00
|
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
|
const inputType = type === 'password' && showPassword ? 'text' : type;
|
|
|
|
|
const hasToggle = type === 'password' && showPasswordToggle;
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="w-full">
|
|
|
|
|
{label && (
|
|
|
|
|
<label
|
|
|
|
|
htmlFor={inputId}
|
2026-02-07 15:02:52 +00:00
|
|
|
className="block text-sm font-medium text-foreground mb-1"
|
2025-12-03 21:56:50 +00:00
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
2026-02-09 22:31:52 +00:00
|
|
|
<div className="relative">
|
|
|
|
|
<input
|
|
|
|
|
id={inputId}
|
|
|
|
|
type={inputType}
|
|
|
|
|
className={cn(
|
feat: frontend improvements — UI polish, player bar, auth flow, i18n
- Header, Sidebar, Toast, Dropdown, EmptyState component refinements
- Auth flow: LoginPage, RegisterPage, AuthInput, AuthLayout improvements
- Player bar: glass effect, progress, track info, controls enhancements
- Dashboard, Discover, Search pages updates
- PlaylistCard, TrackCard component improvements
- Auth store and API interceptors hardening
- i18n: updated en/es/fr locale files
- CSS additions in index.css
- Package.json and vite config updates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:35:44 +00:00
|
|
|
'w-full px-4 py-3 border rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 transition-all duration-[var(--sumi-duration-slow)] ease-[var(--sumi-ease-out)]',
|
|
|
|
|
// Focus glow — subtle but visible
|
|
|
|
|
'focus-visible:shadow-[var(--sumi-shadow-glow)]',
|
|
|
|
|
'bg-[var(--sumi-surface-subtle)] border-[var(--sumi-border-default)] text-foreground placeholder:text-muted-foreground/60',
|
|
|
|
|
'hover:border-[var(--sumi-border-strong)] hover:bg-[var(--sumi-surface-card)]',
|
2026-02-09 22:31:52 +00:00
|
|
|
error
|
|
|
|
|
? 'border-destructive focus-visible:border-destructive'
|
|
|
|
|
: 'focus-visible:border-primary',
|
|
|
|
|
hasToggle ? 'pr-10' : '',
|
|
|
|
|
className,
|
|
|
|
|
)}
|
|
|
|
|
aria-invalid={error ? 'true' : 'false'}
|
|
|
|
|
aria-describedby={error ? `${inputId}-error` : undefined}
|
|
|
|
|
aria-required={props.required ? 'true' : undefined}
|
|
|
|
|
{...props}
|
|
|
|
|
// CRITIQUE FIX #4: Définir autoComplete APRÈS le spread pour éviter qu'il soit écrasé
|
|
|
|
|
// Préférer props.autoComplete s'il est défini, sinon utiliser une valeur par défaut basée sur le type
|
|
|
|
|
autoComplete={
|
|
|
|
|
props.autoComplete !== undefined
|
|
|
|
|
? props.autoComplete
|
|
|
|
|
: type === 'email'
|
|
|
|
|
? 'email'
|
|
|
|
|
: type === 'password'
|
|
|
|
|
? 'current-password'
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
// CRITIQUE FIX #6: S'assurer que l'attribut HTML5 required est présent si props.required est true
|
|
|
|
|
// Le spread {...props} devrait déjà inclure required, mais on le définit explicitement pour être sûr
|
|
|
|
|
required={props.required}
|
|
|
|
|
/>
|
|
|
|
|
{/* Password visibility toggle */}
|
|
|
|
|
{hasToggle && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
|
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-opacity transition-colors duration-[var(--duration-fast)]"
|
|
|
|
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
>
|
|
|
|
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
|
|
|
</button>
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
2026-02-09 22:31:52 +00:00
|
|
|
</div>
|
2025-12-03 21:56:50 +00:00
|
|
|
{error && (
|
|
|
|
|
<p
|
|
|
|
|
id={`${inputId}-error`}
|
2026-02-09 22:31:52 +00:00
|
|
|
className="mt-1 text-sm text-destructive animate-shake"
|
2025-12-03 21:56:50 +00:00
|
|
|
role="alert"
|
|
|
|
|
>
|
|
|
|
|
{error}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|