ExternalLinkPage.tsx 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import { useCallback, useEffect, useRef, type RefObject } from 'react';
  2. import { useParams } from 'react-router-dom';
  3. import { useQuery } from '@tanstack/react-query';
  4. import { Loader2, AlertTriangle } from 'lucide-react';
  5. import { useTranslation } from 'react-i18next';
  6. import { api } from '../api/client';
  7. import { useTheme } from '../contexts/ThemeContext';
  8. export function ExternalLinkPage() {
  9. const { t } = useTranslation();
  10. const { id } = useParams<{ id: string }>();
  11. const { mode } = useTheme();
  12. const iframeRef = useRef<HTMLIFrameElement>(null);
  13. const sendTheme = useThemeMessenger(iframeRef);
  14. const { data: link, isLoading, error } = useQuery({
  15. queryKey: ['external-link', id],
  16. queryFn: () => api.getExternalLink(Number(id)),
  17. enabled: !!id,
  18. });
  19. if (isLoading) {
  20. return (
  21. <div className="flex items-center justify-center h-full">
  22. <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
  23. </div>
  24. );
  25. }
  26. if (error || !link) {
  27. return (
  28. <div className="flex flex-col items-center justify-center h-full gap-4 text-bambu-gray">
  29. <AlertTriangle className="w-12 h-12" />
  30. <p>{t('common.linkNotFound')}</p>
  31. </div>
  32. );
  33. }
  34. return (
  35. <iframe
  36. ref={iframeRef}
  37. src={link.url}
  38. onLoad={sendTheme}
  39. className="h-full w-full border-0"
  40. style={{ colorScheme: mode }}
  41. title={link.name}
  42. sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
  43. />
  44. );
  45. }
  46. /**
  47. * Tell the framed page which theme Bambuddy is showing, so an app built to
  48. * sit in the sidebar (Bambuddy Orders) can match it. Sent when the page
  49. * loads, when the theme changes, and when the page asks for it. Only to the
  50. * link's own origin, and it carries nothing but the theme names.
  51. */
  52. function useThemeMessenger(iframeRef: RefObject<HTMLIFrameElement | null>) {
  53. const { resolvedMode, darkStyle, darkBackground, darkAccent, lightStyle, lightBackground, lightAccent } = useTheme();
  54. const dark = resolvedMode === 'dark';
  55. const style = dark ? darkStyle : lightStyle;
  56. const background = dark ? darkBackground : lightBackground;
  57. const accent = dark ? darkAccent : lightAccent;
  58. const sendTheme = useCallback(() => {
  59. const frame = iframeRef.current;
  60. if (!frame?.contentWindow) return;
  61. let origin: string;
  62. try {
  63. origin = new URL(frame.src).origin;
  64. } catch {
  65. return;
  66. }
  67. frame.contentWindow.postMessage({ type: 'bambuddy:theme', mode: resolvedMode, style, background, accent }, origin);
  68. }, [iframeRef, resolvedMode, style, background, accent]);
  69. useEffect(() => sendTheme(), [sendTheme]);
  70. useEffect(() => {
  71. const onMessage = (e: MessageEvent) => {
  72. if (e.source === iframeRef.current?.contentWindow && e.data?.type === 'bambuddy:theme-request') sendTheme();
  73. };
  74. window.addEventListener('message', onMessage);
  75. return () => window.removeEventListener('message', onMessage);
  76. }, [iframeRef, sendTheme]);
  77. return sendTheme;
  78. }