Download src/components/forms/PinSetupForm.tsx from HonzaH/Letschat: direct link, hf CLI and curl.
- Browser
- Download file 7.27 kB
-
https://huggingface.co/spaces/HonzaH/Letschat/resolve/main/src/components/forms/PinSetupForm.tsx
- Command line
-
hf download hf://spaces/HonzaH/Letschat/src/components/forms/PinSetupForm.tsx
-
curl -L -o PinSetupForm.tsx https://huggingface.co/spaces/HonzaH/Letschat/resolve/main/src/components/forms/PinSetupForm.tsx
7.27 kB
| 'use client' | |
| import { useState } from 'react' | |
| import { useForm } from 'react-hook-form' | |
| import { zodResolver } from '@hookform/resolvers/zod' | |
| import { useRouter, useSearchParams } from 'next/navigation' | |
| import { Button } from 'components/ui/button' | |
| import { z } from 'zod' | |
| import { pinEntrySchema, type PinEntryForm as PinEntryFormType } from 'lib/validations/code' | |
| export function PinSetupForm() { | |
| const [loading, setLoading] = useState(false) | |
| const [error, setError] = useState<string | null>(null) | |
| const [skipPin, setSkipPin] = useState(false) | |
| const [manualCode, setManualCode] = useState('') | |
| const router = useRouter() | |
| const searchParams = useSearchParams() | |
| const codeFromParams = searchParams.get('code') | |
| // Define schema typed for the form to ensure TypeScript knows pin + confirmPin exist | |
| const setupSchema = pinEntrySchema.extend({ | |
| confirmPin: pinEntrySchema.shape.pin | |
| }).refine((data) => data.pin === data.confirmPin, { | |
| message: "PIN se neshoduje", | |
| path: ["confirmPin"], | |
| }) | |
| const { | |
| register, | |
| handleSubmit, | |
| formState: { errors } | |
| } = useForm<z.infer<typeof setupSchema>>({ | |
| resolver: zodResolver(setupSchema) | |
| }) | |
| // Use effectiveCode: prefer query param, otherwise manual input | |
| const effectiveCode = (codeFromParams || manualCode || '').toUpperCase() | |
| const onSubmit = async (data: PinEntryFormType | { pin?: string }) => { | |
| const code = effectiveCode | |
| if (!code) { | |
| setError('Chybí kód místnosti') | |
| return | |
| } | |
| try { | |
| setLoading(true) | |
| setError(null) | |
| const response = await fetch('/api/codes/setup', { | |
| method: 'POST', | |
| credentials: 'include', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| code: code.toUpperCase(), | |
| pin: (data as any).pin ? (data as any).pin : undefined, | |
| userAgent: navigator.userAgent | |
| }) | |
| }) | |
| const result = await response.json() | |
| if (!response.ok) { | |
| setError(result.error || 'Nastala chyba při nastavování') | |
| return | |
| } | |
| if (result.roomId) { | |
| router.push(`/room/${result.roomId}?code=${code}`) | |
| } | |
| } catch (e) { | |
| console.error('PinSetup submit error:', e) | |
| setError('Došlo k neočekávané chybě') | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| const handleSkipPin = async () => { | |
| // Ensure we have a code | |
| if (!effectiveCode) { | |
| setError('Zadejte prosím kód místnosti před pokračováním bez PIN.') | |
| return | |
| } | |
| setSkipPin(true) | |
| await onSubmit({ pin: '' }) | |
| } | |
| if (skipPin) { | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="p-4 bg-yellow-50 rounded-lg"> | |
| <p className="text-yellow-800 text-sm"> | |
| <strong>Upozornění:</strong> Bez PIN ochrany se o místnost přijdete při smazání cookies prohlížeče. | |
| </p> | |
| </div> | |
| <Button onClick={handleSkipPin} className="w-full" disabled={loading} variant="secondary" > | |
| {loading ? 'Vytvářím místnost...' : 'Pokračovat bez PIN'} | |
| </Button> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="space-y-6"> | |
| <div className="p-4 bg-blue-50 rounded-lg"> | |
| <h3 className="font-medium text-blue-900 mb-2">Ochrana místnosti</h3> | |
| <p className="text-blue-800 text-sm"> | |
| Nastavte si 5-číselný PIN pro ochranu přístupu do této místnosti. | |
| Bez PIN se o přístup přijdete při smazání cookies. | |
| </p> | |
| </div> | |
| {/* If no code in query, allow user to type it */} | |
| {!codeFromParams && ( | |
| <div> | |
| <label htmlFor="manualCode" className="block text-sm font-medium text-gray-700 mb-1"> | |
| Kód místnosti (5 znaků) | |
| </label> | |
| <input | |
| value={manualCode} | |
| onChange={(e) => setManualCode(e.target.value.toUpperCase())} | |
| type="text" | |
| id="manualCode" | |
| maxLength={5} | |
| placeholder="A1B2C" | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 uppercase text-center font-mono" | |
| style={{ textTransform: 'uppercase' }} | |
| /> | |
| <p className="mt-1 text-xs text-gray-500">Zadejte kód místnosti, který chcete vytvořit nebo použít.</p> | |
| </div> | |
| )} | |
| <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> | |
| <div> | |
| <label htmlFor="pin" className="block text-sm font-medium text-gray-700 mb-2"> | |
| Nový PIN (5 číslic) | |
| </label> | |
| <input | |
| {...register('pin')} | |
| type="password" | |
| id="pin" | |
| maxLength={5} | |
| placeholder="12345" | |
| autoComplete="new-password" | |
| inputMode="numeric" | |
| pattern="[0-9]*" | |
| aria-label="Nový PIN (5 číslic)" | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 text-center text-xl font-mono tracking-widest" | |
| /> | |
| {errors?.pin && ( | |
| <p className="mt-1 text-sm text-red-600">{errors.pin.message}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label htmlFor="confirmPin" className="block text-sm font-medium text-gray-700 mb-2"> | |
| Potvrdit PIN | |
| </label> | |
| <input | |
| {...register('confirmPin')} | |
| type="password" | |
| id="confirmPin" | |
| maxLength={5} | |
| placeholder="12345" | |
| autoComplete="new-password" | |
| inputMode="numeric" | |
| pattern="[0-9]*" | |
| aria-label="Potvrzení PIN" | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 text-center text-xl font-mono tracking-widest" | |
| /> | |
| {errors?.confirmPin && ( | |
| <p className="mt-1 text-sm text-red-600">{errors.confirmPin.message}</p> | |
| )} | |
| </div> | |
| {error && ( | |
| <div className="p-3 text-sm text-red-600 bg-red-50 rounded-md"> | |
| {error} | |
| </div> | |
| )} | |
| <div className="space-y-2"> | |
| <Button type="submit" className="w-full" disabled={loading}> | |
| {loading ? 'Nastavuji PIN...' : 'Vytvořit místnost s PIN'} | |
| </Button> | |
| <button | |
| type="button" | |
| onClick={handleSkipPin} | |
| className="w-full px-4 py-2 text-sm text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50" | |
| > | |
| Pokračovat bez PIN ochrany | |
| </button> | |
| </div> | |
| </form> | |
| <div className="text-center"> | |
| <button | |
| type="button" | |
| onClick={() => router.push('/')} | |
| className="text-sm text-gray-600 hover:text-gray-800" | |
| > | |
| ← Zpět na zadání kódu | |
| </button> | |
| </div> | |
| </div> | |
| ) | |
| } |