Spaces:
Sleeping
Sleeping
| import React, { useRef, useState } from 'react'; | |
| import { ArrowRight, KeyRound, User, Languages } from 'lucide-react'; | |
| interface CodeInputProps { | |
| onComplete: (code: string, name?: string, language?: string) => void; | |
| isLoading?: boolean; | |
| } | |
| const LANGUAGES = [ | |
| { label: 'No Dubbing (Original)', value: '' }, | |
| { label: 'Spanish', value: 'Spanish' }, | |
| { label: 'French', value: 'French' }, | |
| { label: 'German', value: 'German' }, | |
| { label: 'Hindi', value: 'Hindi' }, | |
| { label: 'Japanese', value: 'Japanese' }, | |
| { label: 'Chinese (Mandarin)', value: 'Chinese' }, | |
| { label: 'Portuguese', value: 'Portuguese' }, | |
| ]; | |
| export const CodeInput: React.FC<CodeInputProps> = ({ onComplete, isLoading }) => { | |
| const [code, setCode] = useState<string[]>(new Array(6).fill('')); | |
| const [name, setName] = useState(''); | |
| const [language, setLanguage] = useState(''); | |
| const inputsRef = useRef<(HTMLInputElement | null)[]>([]); | |
| const handleChange = (index: number, value: string) => { | |
| if (!/^\d*$/.test(value)) return; | |
| const newCode = [...code]; | |
| // Take only the last character if user somehow pastes or types multiple | |
| newCode[index] = value.slice(-1); | |
| setCode(newCode); | |
| if (value && index < 5) { | |
| inputsRef.current[index + 1]?.focus(); | |
| } | |
| }; | |
| const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => { | |
| if (e.key === 'Backspace' && !code[index] && index > 0) { | |
| inputsRef.current[index - 1]?.focus(); | |
| } | |
| }; | |
| const handlePaste = (e: React.ClipboardEvent) => { | |
| e.preventDefault(); | |
| const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').split(''); | |
| if (pastedData.length > 0) { | |
| const newCode = [...code]; | |
| pastedData.slice(0, 6).forEach((digit, i) => { | |
| newCode[i] = digit; | |
| }); | |
| setCode(newCode); | |
| const lastIndex = Math.min(pastedData.length - 1, 5); | |
| inputsRef.current[lastIndex]?.focus(); | |
| } | |
| }; | |
| const handleSubmit = (e?: React.FormEvent) => { | |
| e?.preventDefault(); | |
| const codeStr = code.join(''); | |
| if (codeStr.length === 6) { | |
| onComplete(codeStr, name || 'Guest', language); | |
| } | |
| }; | |
| return ( | |
| <form onSubmit={handleSubmit} className="w-full max-w-md flex flex-col items-center space-y-8 animate-fade-in"> | |
| <div className="text-center space-y-2"> | |
| <div className="bg-cyan-500/10 w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-6 border border-cyan-500/20 shadow-[0_0_30px_-5px_rgba(6,182,212,0.3)]"> | |
| <KeyRound className="w-8 h-8 text-cyan-400" /> | |
| </div> | |
| <h2 className="text-3xl font-bold text-white tracking-tight">Connect with Code</h2> | |
| <p className="text-zinc-400 text-sm">Enter details to join the secure channel.</p> | |
| </div> | |
| <div className="w-full space-y-4"> | |
| <div className="w-full relative group"> | |
| <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> | |
| <User className="h-5 w-5 text-zinc-500 group-focus-within:text-cyan-400 transition-colors" /> | |
| </div> | |
| <input | |
| type="text" | |
| value={name} | |
| onChange={(e) => setName(e.target.value)} | |
| placeholder="Your Name (Optional)" | |
| className="w-full bg-zinc-900/50 border-2 border-zinc-800 text-white pl-11 pr-4 py-3.5 rounded-xl focus:outline-none focus:border-cyan-500/50 focus:bg-zinc-900/80 transition-all placeholder:text-zinc-600" | |
| /> | |
| </div> | |
| <div className="w-full relative group"> | |
| <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> | |
| <Languages className="h-5 w-5 text-zinc-500 group-focus-within:text-cyan-400 transition-colors" /> | |
| </div> | |
| <select | |
| value={language} | |
| onChange={(e) => setLanguage(e.target.value)} | |
| className="w-full bg-zinc-900/50 border-2 border-zinc-800 text-white pl-11 pr-4 py-3.5 rounded-xl focus:outline-none focus:border-cyan-500/50 focus:bg-zinc-900/80 transition-all appearance-none cursor-pointer" | |
| > | |
| {LANGUAGES.map(lang => ( | |
| <option key={lang.value} value={lang.value} className="bg-zinc-900 text-white"> | |
| {lang.label} | |
| </option> | |
| ))} | |
| </select> | |
| <div className="absolute inset-y-0 right-0 pr-4 flex items-center pointer-events-none"> | |
| <svg className="w-4 h-4 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex gap-2 sm:gap-3 justify-center" onPaste={handlePaste}> | |
| {code.map((digit, idx) => ( | |
| <input | |
| key={idx} | |
| ref={el => inputsRef.current[idx] = el} | |
| type="text" | |
| inputMode="numeric" | |
| maxLength={1} | |
| autoFocus={idx === 0} | |
| value={digit} | |
| onChange={(e) => handleChange(idx, e.target.value)} | |
| onKeyDown={(e) => handleKeyDown(idx, e)} | |
| disabled={isLoading} | |
| className={`w-10 h-12 sm:w-12 sm:h-14 text-center text-xl sm:text-2xl font-mono font-bold rounded-xl border-2 transition-all duration-200 outline-none | |
| ${digit | |
| ? 'border-cyan-500 bg-cyan-950/30 text-cyan-100 shadow-[0_0_15px_-3px_rgba(6,182,212,0.3)]' | |
| : 'border-zinc-800 bg-zinc-900/50 text-zinc-500 focus:border-zinc-600 focus:bg-zinc-900' | |
| } | |
| disabled:opacity-50 disabled:cursor-not-allowed | |
| `} | |
| /> | |
| ))} | |
| </div> | |
| <button | |
| type="submit" | |
| disabled={code.some(d => d === '') || isLoading} | |
| className="group relative flex items-center justify-center gap-2 px-8 py-3 w-full rounded-xl font-semibold text-white bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 disabled:from-zinc-800 disabled:to-zinc-800 disabled:text-zinc-500 disabled:cursor-not-allowed transition-all duration-300 shadow-lg shadow-cyan-900/20" | |
| > | |
| {isLoading ? ( | |
| <span className="flex items-center gap-2"> | |
| <svg className="animate-spin h-5 w-5 text-white/80" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> | |
| <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"></path> | |
| </svg> | |
| Connecting... | |
| </span> | |
| ) : ( | |
| <> | |
| Start Secure Call <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" /> | |
| </> | |
| )} | |
| </button> | |
| </form> | |
| ); | |
| }; |