Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
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}