Zoom Integration Frontend Code

PHOTO EMBED

Wed Jul 23 2025 09:00:10 GMT+0000 (Coordinated Universal Time)

Saved by @Rishi1808

import React, { useState, useRef, useEffect } from 'react';
import { Loader2, Video, LogOut } from 'lucide-react';
import clsx from 'clsx';

/* ------------------------------------------------------------------ */
/*  TYPES & GLOBALS                                                   */
/* ------------------------------------------------------------------ */
declare global {
  interface Window {
    ZoomMtgEmbedded?: any;
  }
}

/* ------------------------------------------------------------------ */
/*  CONSTANTS                                                         */
/* ------------------------------------------------------------------ */
const ZOOM_SDK_VERSION = '3.11.2'; // keep in one place for easy upgrades
const ZOOM_SDK_LIBS = [
  `https://source.zoom.us/${ZOOM_SDK_VERSION}/lib/vendor/react.min.js`,
  `https://source.zoom.us/${ZOOM_SDK_VERSION}/lib/vendor/react-dom.min.js`,
  `https://source.zoom.us/${ZOOM_SDK_VERSION}/lib/vendor/redux.min.js`,
  `https://source.zoom.us/${ZOOM_SDK_VERSION}/lib/vendor/redux-thunk.min.js`,
  `https://source.zoom.us/${ZOOM_SDK_VERSION}/lib/vendor/lodash.min.js`,
  `https://source.zoom.us/${ZOOM_SDK_VERSION}/zoom-meeting-embedded-${ZOOM_SDK_VERSION}.min.js`,
];

/* ------------------------------------------------------------------ */
/*  COMPONENT                                                         */
/* ------------------------------------------------------------------ */
const Zoom: React.FC = () => {
  /* form state ----------------------------------------------------- */
  const [meetingNumber, setMeetingNumber] = useState('');
  const [userName, setUserName] = useState('React User');
  const [meetingPassword, setMeetingPassword] = useState('');
  const [role, setRole] = useState<number>(0); // 0 attendee | 1 host

  /* ui state ------------------------------------------------------- */
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [meetingStarted, setMeetingStarted] = useState(false);

  /* refs ----------------------------------------------------------- */
  const meetingSDKElementRef = useRef<HTMLDivElement>(null);
  const clientRef = useRef<any>(null); // Zoom client instance

  /* ------------------------------------------------------------------ */
  /*  SIDE-EFFECT: Load Zoom SDK once on mount                          */
  /* ------------------------------------------------------------------ */
  useEffect(() => {
    if (window.ZoomMtgEmbedded) {
      console.log('[Zoom] SDK already present');
      return;
    }

    console.log('[Zoom] Loading Zoom SDK…');
    const loadScript = (src: string) =>
      new Promise<void>((resolve, reject) => {
        const s = document.createElement('script');
        s.src = src;
        s.async = false;          // load in order
        s.onload = () => {
          console.log(`[Zoom] ✓ loaded ${src}`);
          resolve();
        };
        s.onerror = () => {
          console.error(`[Zoom] ✗ failed to load ${src}`);
          reject();
        };
        document.body.appendChild(s);
      });

    (async () => {
      try {
        for (const lib of ZOOM_SDK_LIBS) await loadScript(lib);
        console.log('[Zoom] All SDK scripts loaded');
      } catch {
        setError('Failed to load Zoom SDK. Check console for details.');
      }
    })();
  }, []);

  /* ------------------------------------------------------------------ */
  /*  HELPERS                                                           */
  /* ------------------------------------------------------------------ */
  const getSignature = async () => {
    console.log('[Zoom] Fetching signature…');
    setLoading(true);
    setError(null);
    try {
      const res = await fetch(
        `http://localhost:80/api/zoom/signature?meetingNumber=${meetingNumber}&role=${role}`,
      );
      if (!res.ok) throw new Error((await res.json())?.message);
      const data = await res.json();
      console.log('[Zoom] Signature fetched:', data);
      return data;
    } catch (err: any) {
      console.error('[Zoom] Signature error:', err);
      setError(err?.message ?? 'Failed to get signature');
      return null;
    } finally {
      setLoading(false);
    }
  };

  const startMeeting = async () => {
    console.log('[Zoom] startMeeting() called');
    if (!meetingNumber.trim() || !userName.trim()) {
      setError('Meeting Number & Name required.');
      return;
    }
    if (!meetingSDKElementRef.current) {
      setError('SDK mount element not found.');
      return;
    }
    if (!window.ZoomMtgEmbedded) {
      setError('Zoom SDK still loading, please try again in a moment.');
      return;
    }

    const sigData = await getSignature();
    if (!sigData) return;

    const { signature, sdkKey } = sigData;
    clientRef.current = window.ZoomMtgEmbedded.createClient();

    try {
      console.log('[Zoom] Initializing client…');
      await clientRef.current.init({
        zoomAppRoot: meetingSDKElementRef.current,
        language: 'en-US',
        patchJsMedia: true,
      });
      console.log('[Zoom] Client initialized, joining…');
      await clientRef.current.join({
        sdkKey,
        signature,
        meetingNumber,
        userName,
        password: meetingPassword,
        role,
      });
      console.log('[Zoom] Joined meeting');
      setMeetingStarted(true);
    } catch (err: any) {
      console.error('[Zoom] Unable to start meeting:', err);
      setError(err?.message ?? 'Unable to start meeting');
    }
  };

  const leaveMeeting = async () => {
    console.log('[Zoom] leaveMeeting() called');
    if (!meetingStarted || !clientRef.current) return;
    try {
      await clientRef.current.leave();
      console.log('[Zoom] Left meeting');
      setMeetingStarted(false);
    } catch (err: any) {
      console.error('[Zoom] Failed to leave:', err);
      setError(err?.message ?? 'Failed to leave');
    }
  };

  /* ------------------------------------------------------------------ */
  /*  RENDER                                                            */
  /* ------------------------------------------------------------------ */
  return (
    <main className="min-h-screen relative flex items-center justify-center bg-gradient-to-br from-blue-950 via-indigo-900 to-fuchsia-900 overflow-hidden">
      {/* floating blur blobs */}
      <div className="absolute inset-0">
        {Array.from({ length: 10 }).map((_, i) => (
          <div
            // purely decorative blobs
            key={i}
            className={clsx(
              'absolute rounded-full blur-2xl opacity-25',
              i % 2 ? 'bg-blue-600' : 'bg-violet-700',
            )}
            style={{
              width: `${Math.random() * 18 + 12}rem`,
              height: `${Math.random() * 18 + 12}rem`,
              top: `${Math.random() * 100}%`,
              left: `${Math.random() * 100}%`,
              animation: `pulse ${Math.random() * 12 + 8}s ease-in-out infinite`,
            }}
          />
        ))}
      </div>

      <section className="relative z-10 w-full max-w-3xl mx-auto p-6">
        <h1 className="text-center text-4xl font-bold text-white mb-8 flex items-center justify-center gap-2">
          <Video className="w-8 h-8 text-blue-400" />
          Zoom Meeting
        </h1>

        {/* CARD ---------------------------------------------------- */}
        <div className="backdrop-blur-xl bg-white/5 border border-white/[0.08] rounded-2xl shadow-2xl overflow-hidden">
          {!meetingStarted ? (
            <form
              onSubmit={(e) => {
                e.preventDefault();
                startMeeting();
              }}
              className="grid gap-6 p-8"
            >
              {/* inputs */}
              <Input
                label="Meeting Number"
                value={meetingNumber}
                onChange={setMeetingNumber}
                placeholder="e.g. 123456789"
                autoFocus
              />
              <Input
                label="Your Name"
                value={userName}
                onChange={setUserName}
                placeholder="Enter display name"
              />
              <Input
                label="Password (optional)"
                type="password"
                value={meetingPassword}
                onChange={setMeetingPassword}
                placeholder="Meeting passcode"
              />

              <div>
                <label className="block text-sm font-medium text-white/80 mb-1">
                  Role
                </label>
                <select
                  value={role}
                  onChange={(e) => setRole(parseInt(e.target.value, 10))}
                  className="w-full rounded-lg bg-white/10 border border-white/20 px-3 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:outline-none"
                >
                  <option value={0}>Attendee</option>
                  <option value={1}>Host</option>
                </select>
              </div>

              {error && (
                <p className="text-center text-sm text-red-400 -mt-3">
                  {error}
                </p>
              )}

              <button
                type="submit"
                disabled={loading}
                className={clsx(
                  'w-full flex items-center justify-center gap-2 rounded-lg py-3 font-semibold transition',
                  loading
                    ? 'bg-blue-400 cursor-not-allowed'
                    : 'bg-gradient-to-r from-blue-600 to-violet-600 hover:brightness-110',
                )}
              >
                {loading ? (
                  <>
                    <Loader2 className="w-5 h-5 animate-spin" /> Starting…
                  </>
                ) : (
                  'Join Meeting'
                )}
              </button>
            </form>
          ) : (
            <div className="p-10 flex flex-col items-center gap-6 text-white">
              <p className="text-lg">
                🎉 Connected as <span className="font-semibold">{userName}</span>
              </p>
              <button
                onClick={leaveMeeting}
                className="flex items-center gap-2 bg-gradient-to-r from-red-600 to-red-700 hover:brightness-110 px-6 py-3 rounded-lg font-semibold"
              >
                <LogOut className="w-5 h-5" /> Leave
              </button>
            </div>
          )}

          {/* Zoom SDK mount */}
          <div
            id="meetingSDKElement"
            ref={meetingSDKElementRef}
            className={clsx(
              'w-full overflow-hidden transition-all duration-500',
              meetingStarted ? 'h-[70vh]' : 'h-0',
            )}
          />
        </div>
      </section>
    </main>
  );
};

/* ------------------------------------------------------------------ */
/*  REUSABLE INPUT                                                   */
/* ------------------------------------------------------------------ */
interface InputProps {
  label: string;
  value: string;
  onChange: (v: string) => void;
  type?: React.HTMLInputTypeAttribute;
  placeholder?: string;
  autoFocus?: boolean;
}

const Input: React.FC<InputProps> = ({
  label,
  value,
  onChange,
  type = 'text',
  placeholder,
  autoFocus,
}) => (
  <div className="space-y-1">
    <label className="block text-sm font-medium text-white/80">
      {label}:
    </label>
    <input
      type={type}
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder={placeholder}
      autoFocus={autoFocus}
      className="w-full rounded-lg bg-white/10 border border-white/20 px-3 py-2 text-white placeholder-white/50 focus:ring-2 focus:ring-blue-500 focus:outline-none"
    />
  </div>
);

export default Zoom;
content_copyCOPY