141 lines
5.0 KiB
TypeScript
141 lines
5.0 KiB
TypeScript
'use client';
|
||
|
||
import React, { useEffect, useRef } from 'react';
|
||
|
||
interface WhiteboardIframeProps {
|
||
boardId: string;
|
||
username?: string;
|
||
token?: string;
|
||
/** Только ментор может очищать холст. */
|
||
isMentor?: boolean;
|
||
onToggleView?: () => void;
|
||
showingBoard?: boolean;
|
||
className?: string;
|
||
style?: React.CSSProperties;
|
||
}
|
||
|
||
/**
|
||
* Интерактивная доска Excalidraw + Yjs.
|
||
* Iframe создаётся императивно один раз — React не пересоздаёт его при переключении.
|
||
*/
|
||
export function WhiteboardIframe({
|
||
boardId,
|
||
username = 'Пользователь',
|
||
token: tokenProp,
|
||
isMentor = false,
|
||
showingBoard = true,
|
||
className = '',
|
||
style = {},
|
||
}: WhiteboardIframeProps) {
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||
const createdRef = useRef(false);
|
||
|
||
useEffect(() => {
|
||
if (typeof window === 'undefined' || !containerRef.current || !boardId) return;
|
||
if (createdRef.current) return;
|
||
|
||
const token = tokenProp ?? localStorage.getItem('access_token') ?? '';
|
||
const apiBase = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:8123/api';
|
||
const apiUrl = apiBase.replace(/\/api\/?$/, '') || 'http://127.0.0.1:8123';
|
||
// Вариант 1: отдельный URL доски (как LiveKit) — если app.uchill.online отдаёт один Next без nginx по путям
|
||
const excalidrawBaseUrl = (process.env.NEXT_PUBLIC_EXCALIDRAW_URL || '').trim().replace(/\/?$/, '');
|
||
const url: URL = excalidrawBaseUrl
|
||
? new URL(excalidrawBaseUrl.startsWith('http') ? excalidrawBaseUrl + '/' : `${window.location.origin}/${excalidrawBaseUrl}/`)
|
||
: (() => {
|
||
const path = process.env.NEXT_PUBLIC_EXCALIDRAW_PATH || '';
|
||
if (path) {
|
||
return new URL((path.startsWith('/') ? path : '/' + path) + '/', window.location.origin);
|
||
}
|
||
return new URL('/', `${window.location.protocol}//${window.location.hostname}:${process.env.NEXT_PUBLIC_EXCALIDRAW_PORT || '3001'}`);
|
||
})();
|
||
url.searchParams.set('boardId', boardId);
|
||
url.searchParams.set('apiUrl', apiUrl);
|
||
if (token) url.searchParams.set('token', token);
|
||
if (isMentor) url.searchParams.set('isMentor', '1');
|
||
|
||
const iframeSrc = excalidrawBaseUrl && excalidrawBaseUrl.startsWith('http')
|
||
? url.toString()
|
||
: `${url.origin}${url.pathname.replace(/\/?$/, '/')}?${url.searchParams.toString()}`;
|
||
|
||
const iframe = document.createElement('iframe');
|
||
iframe.src = iframeSrc;
|
||
iframe.style.cssText = 'width:100%;height:100%;border:none;display:block';
|
||
iframe.title = 'Интерактивная доска (Excalidraw)';
|
||
iframe.setAttribute('allow', 'camera; microphone; fullscreen');
|
||
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-modals');
|
||
|
||
const decodeName = (raw: string): string => {
|
||
if (!raw || typeof raw !== 'string') return raw;
|
||
try {
|
||
if (/%[0-9A-Fa-f]{2}/.test(raw)) return decodeURIComponent(raw);
|
||
return raw;
|
||
} catch {
|
||
return raw;
|
||
}
|
||
};
|
||
|
||
const sendUsername = () => {
|
||
if (iframe.contentWindow) {
|
||
try {
|
||
iframe.contentWindow.postMessage(
|
||
{ type: 'excalidraw-username', username: decodeName(username) },
|
||
url.origin
|
||
);
|
||
} catch (_) {}
|
||
}
|
||
};
|
||
|
||
iframe.onload = () => {
|
||
sendUsername();
|
||
setTimeout(sendUsername, 500);
|
||
};
|
||
|
||
containerRef.current.appendChild(iframe);
|
||
iframeRef.current = iframe;
|
||
createdRef.current = true;
|
||
|
||
setTimeout(sendUsername, 300);
|
||
return () => {
|
||
createdRef.current = false;
|
||
iframeRef.current = null;
|
||
iframe.remove();
|
||
};
|
||
}, [boardId, tokenProp, isMentor]);
|
||
|
||
useEffect(() => {
|
||
if (!iframeRef.current?.contentWindow || !createdRef.current) return;
|
||
const excalidrawBaseUrl = (process.env.NEXT_PUBLIC_EXCALIDRAW_URL || '').trim().replace(/\/?$/, '');
|
||
const targetOrigin = excalidrawBaseUrl.startsWith('http')
|
||
? new URL(excalidrawBaseUrl).origin
|
||
: (() => {
|
||
const path = process.env.NEXT_PUBLIC_EXCALIDRAW_PATH || '';
|
||
if (path) return window.location.origin;
|
||
return `${window.location.protocol}//${window.location.hostname}:${process.env.NEXT_PUBLIC_EXCALIDRAW_PORT || '3001'}`;
|
||
})();
|
||
const decodeName = (raw: string): string => {
|
||
if (!raw || typeof raw !== 'string') return raw;
|
||
try {
|
||
if (/%[0-9A-Fa-f]{2}/.test(raw)) return decodeURIComponent(raw);
|
||
return raw;
|
||
} catch {
|
||
return raw;
|
||
}
|
||
};
|
||
try {
|
||
iframeRef.current.contentWindow.postMessage(
|
||
{ type: 'excalidraw-username', username: decodeName(username) },
|
||
targetOrigin
|
||
);
|
||
} catch (_) {}
|
||
}, [username]);
|
||
|
||
return (
|
||
<div
|
||
ref={containerRef}
|
||
className={className}
|
||
style={{ width: '100%', height: '100%', position: 'relative', ...style }}
|
||
/>
|
||
);
|
||
}
|