Files
fspecii 325046eaf2 Apply i18n to all components, add collapsible sidebar, playback speed control, inline title editing, and ConfirmDialog for deletions
Phase 4: i18n all 15 components from PR #19. Sidebar gains collapse/expand
toggle. Player gets playback speed selector (0.25x-2.0x) fixing bug B1
(hardcoded Chinese '正常'). SongList adds inline title editing and model
version badge. Delete actions now use ConfirmDialog instead of
window.confirm. Volume persisted to localStorage. Training nav item
deferred to Phase 7.
2026-02-08 18:35:09 +02:00

123 lines
4.5 KiB
TypeScript

import React, { useState } from 'react';
import { X, User, Sparkles } from 'lucide-react';
import { useI18n } from '../context/I18nContext';
interface UsernameModalProps {
isOpen: boolean;
onSubmit: (username: string) => Promise<void>;
}
export const UsernameModal: React.FC<UsernameModalProps> = ({ isOpen, onSubmit }) => {
const { t } = useI18n();
const [username, setUsername] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
const trimmed = username.trim();
if (trimmed.length < 2) {
setError(t('usernameMinLength'));
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
setError(t('usernameInvalidChars'));
return;
}
setIsLoading(true);
try {
await onSubmit(trimmed);
} catch (err) {
setError(err instanceof Error ? err.message : t('failedToSetUsername'));
} finally {
setIsLoading(false);
}
};
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" />
{/* Modal */}
<div className="relative w-full max-w-md bg-zinc-900 rounded-2xl shadow-2xl border border-white/10 overflow-hidden">
{/* Header gradient */}
<div className="h-2 bg-gradient-to-r from-pink-500 via-purple-500 to-blue-500" />
<div className="p-8">
{/* Logo */}
<div className="flex justify-center mb-6">
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center shadow-lg">
<Sparkles className="w-8 h-8 text-white" />
</div>
</div>
{/* Title */}
<h2 className="text-2xl font-bold text-center text-white mb-2">
{t('welcomeTitle')}
</h2>
<p className="text-zinc-400 text-center mb-8">
{t('welcomeSubtitle')}
</p>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-zinc-300 mb-2">
{t('yourName')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="w-5 h-5 text-zinc-500" />
</div>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t('enterYourName')}
className="w-full pl-10 pr-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-pink-500 focus:border-transparent transition-all"
autoFocus
disabled={isLoading}
/>
</div>
{error && (
<p className="mt-2 text-sm text-red-400">{error}</p>
)}
</div>
<button
type="submit"
disabled={isLoading || !username.trim()}
className="w-full py-3 bg-gradient-to-r from-pink-500 to-purple-600 text-white font-semibold rounded-xl hover:from-pink-600 hover:to-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all transform hover:scale-[1.02] active:scale-[0.98]"
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
{t('gettingStarted')}
</span>
) : (
t('getStarted')
)}
</button>
</form>
{/* Footer */}
<p className="mt-6 text-xs text-zinc-500 text-center">
{t('yourMusicYourWay')}
</p>
</div>
</div>
</div>
);
};