Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 114 lines 3.8 kB view raw
1'use client'; 2 3import { useState, useMemo, useEffect, useCallback } from 'react'; 4import { useTranslations } from 'next-intl'; 5import { useSearchParams } from 'next/navigation'; 6import { useAuth } from '@/components/auth-provider'; 7import { toast } from 'sonner'; 8 9export function EmbedBuilder() { 10 const t = useTranslations('embedBuilder'); 11 const { session } = useAuth(); 12 const searchParams = useSearchParams(); 13 const [identifier, setIdentifier] = useState(searchParams.get('handle') ?? session?.handle ?? ''); 14 const [debouncedIdentifier, setDebouncedIdentifier] = useState(identifier); 15 16 useEffect(() => { 17 const timer = setTimeout(() => { 18 setDebouncedIdentifier(identifier); 19 }, 500); 20 return () => clearTimeout(timer); 21 }, [identifier]); 22 23 const embedCode = useMemo(() => { 24 if (!debouncedIdentifier.trim()) return ''; 25 26 const isDid = debouncedIdentifier.startsWith('did:'); 27 const dataAttr = isDid 28 ? `data-did="${debouncedIdentifier}"` 29 : `data-handle="${debouncedIdentifier}"`; 30 31 return `<script src="https://sifa.id/embed.js" ${dataAttr}></script>`; 32 }, [debouncedIdentifier]); 33 34 const [iframeHeight, setIframeHeight] = useState(300); 35 36 const handleMessage = useCallback((e: MessageEvent) => { 37 if (e.data?.type === 'sifa-embed-resize' && typeof e.data.height === 'number') { 38 setIframeHeight(e.data.height); 39 } 40 }, []); 41 42 useEffect(() => { 43 window.addEventListener('message', handleMessage); 44 return () => window.removeEventListener('message', handleMessage); 45 }, [handleMessage]); 46 47 function handleCopy() { 48 void navigator.clipboard.writeText(embedCode).then(() => { 49 toast.success(t('copied')); 50 }); 51 } 52 53 return ( 54 <div className="grid gap-8 lg:grid-cols-2"> 55 {/* Left column: config form */} 56 <div className="space-y-6"> 57 <div> 58 <label htmlFor="embed-identifier" className="block text-sm font-medium"> 59 {t('identifierLabel')} 60 </label> 61 <input 62 id="embed-identifier" 63 type="text" 64 aria-label={t('identifierLabel')} 65 value={identifier} 66 onChange={(e) => setIdentifier(e.target.value)} 67 placeholder="alice.bsky.social" 68 className="mt-1 block w-full rounded-md border border-border bg-background px-3 py-2 text-sm" 69 /> 70 </div> 71 72 <p className="text-sm text-muted-foreground">{t('themeNote')}</p> 73 74 {debouncedIdentifier.trim() && ( 75 <div> 76 <label className="block text-sm font-medium">{t('codeLabel')}</label> 77 <pre 78 data-testid="embed-code" 79 className="mt-1 overflow-x-auto rounded-md bg-muted p-3 text-xs" 80 > 81 {embedCode} 82 </pre> 83 <button 84 type="button" 85 onClick={handleCopy} 86 className="mt-2 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground" 87 > 88 {t('copy')} 89 </button> 90 </div> 91 )} 92 </div> 93 94 {/* Right column: preview */} 95 <div> 96 <p className="text-sm font-medium">{t('previewLabel')}</p> 97 <div className="mt-2 rounded-md border border-border"> 98 {debouncedIdentifier.trim() ? ( 99 <iframe 100 src={`/embed/${encodeURIComponent(debouncedIdentifier)}`} 101 title={t('previewTitle')} 102 className="w-full rounded-md" 103 style={{ height: `${iframeHeight}px` }} 104 /> 105 ) : ( 106 <div className="flex h-[300px] items-center justify-center text-muted-foreground"> 107 {t('enterHandle')} 108 </div> 109 )} 110 </div> 111 </div> 112 </div> 113 ); 114}