vkashti / app / admin / quiz / components / QuizContent.tsx
QuizContent.tsx
Raw
'use client';
import { useState, useRef, useCallback, useEffect } from 'react';
import { FiEdit2, FiPlay } from 'react-icons/fi';
import { QRCode } from 'react-qrcode-logo';
import { useQuiz } from '../QuizProvider';
import { Quiz } from '../[id]/page';
import { RoundTable } from './RoundTable';
import { Scoreboard } from './Scoreboard';
import { LatestAnswers } from './LatestAnswers';
import OnlineTeams from '../OnlineTeams';
import { slideToQuestionNumber, getCurrentRound } from '@/utils/quizHelpers';
import { useSearchParams } from 'next/navigation';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
import { createClient } from '@/utils/supabase/client';

interface QuizContentProps {
  quiz: Quiz;
}

export function QuizContent({ quiz }: QuizContentProps) {
  const { groupedAnswers, teamTotals, answers } = useQuiz();
  const [showQuiz, setShowQuiz] = useState(false);
  const iframeContainerRef = useRef<HTMLDivElement>(null);
  const searchParams = useSearchParams();
  const supabase = createClient();
  
  // Get the current slide from URL query parameter or default to 1
  const currentSlide = parseInt(searchParams?.get('slide') || '1');
  
  // Calculate question number from slide number
  const currentQuestionNumber = slideToQuestionNumber(currentSlide);
  
  // Broadcast current slide/question to all participants
  useEffect(() => {
    const channel = supabase.channel('admin-broadcast');
    
    const broadcastCurrentQuestion = async () => {
      try {
        await channel.send({
          type: 'broadcast',
          event: 'slide-change',
          payload: {
            quizId: quiz.id,
            currentSlide,
            currentQuestionNumber
          }
        });
        console.log("Successfully broadcast slide change to participants");
      } catch (error) {
        console.error("Error broadcasting slide change:", error);
      }
    };
    
    // Listen for requests from newly joined participants
    channel.on('broadcast', { event: 'request-slide-state' }, async (payload) => {
      if (payload.payload.quizId === quiz.id) {
        console.log(`Team ${payload.payload.teamName} requested current slide state`);
        
        try {
          // Add slight random delay to avoid overwhelming the channel if multiple teams join at once
          const randomDelay = Math.random() * 500; // Random delay between 0-500ms
          await new Promise(resolve => setTimeout(resolve, randomDelay));
          
          // Respond with current slide state
          await channel.send({
            type: 'broadcast',
            event: 'slide-current-state',
            payload: {
              quizId: quiz.id,
              currentSlide,
              currentQuestionNumber,
              // Include the team name so they know it's a response to their request
              requestedBy: payload.payload.teamName,
              timestamp: new Date().toISOString()
            }
          });
          console.log(`Sent current slide state to team ${payload.payload.teamName}`);
        } catch (error) {
          console.error(`Error sending slide state to team ${payload.payload.teamName}:`, error);
        }
      }
    });
    
    // Subscribe to the channel
    channel.subscribe(status => {
      if (status === 'SUBSCRIBED') {
        broadcastCurrentQuestion();
      }
    });
    
    // Cleanup on unmount
    return () => {
      supabase.removeChannel(channel);
    };
  }, [currentSlide, currentQuestionNumber, quiz.id, supabase]);

  // Ensure the URL has the slide parameter when the component first loads
  useEffect(() => {
    // If there's no slide parameter in the URL, add it
    if (!searchParams?.has('slide')) {
      const url = new URL(window.location.href);
      url.searchParams.set('slide', '1');
      window.history.replaceState({}, '', url.toString());
    }
  }, [searchParams]);

  // Listen for popstate events (browser back/forward buttons)
  useEffect(() => {
    const handlePopState = () => {
      const urlParams = new URLSearchParams(window.location.search);
      const slideParam = urlParams.get('slide');
      if (slideParam) {
        const newSlide = parseInt(slideParam);
        // Update iframe to the new slide
        if (iframeContainerRef.current) {
          const existingIframe = iframeContainerRef.current.querySelector('iframe');
          if (existingIframe) {
            try {
              // Try to use the Canva API to navigate
              existingIframe.contentWindow?.postMessage(
                { type: 'showSlide', slideNumber: newSlide },
                '*'
              );
              
              // Backup method: Update the hash in the URL
              const currentSrc = existingIframe.src;
              const baseUrl = currentSrc.split('#')[0];
              
              if (existingIframe.contentWindow?.location) {
                existingIframe.contentWindow.location.replace(`${baseUrl}#${newSlide}`);
              } else {
                existingIframe.src = `${baseUrl}#${newSlide}`;
              }
            } catch (e) {
              console.warn('Error navigating iframe on popstate', e);
              const currentSrc = existingIframe.src;
              const baseUrl = currentSrc.split('#')[0];
              existingIframe.src = `${baseUrl}#${newSlide}`;
            }
          }
        }
      }
    };

    window.addEventListener('popstate', handlePopState);
    return () => {
      window.removeEventListener('popstate', handlePopState);
    };
  }, []);

  // Transform Canva URL to embed format with slide parameter
  const getEmbedUrl = useCallback((url: string, slide: number = 1) => {
    if (!url) return '';
    const designMatch = url.match(/design\/(.*?)\/(.*?)\/view/);
    if (designMatch) {
      const [_, designId, uniqueId] = designMatch;
      
      // For Canva, try using the hash parameter which seems to work for slide navigation
      return `https://www.canva.com/design/${designId}/${uniqueId}/view?embed#${slide}`;
    }
    return url + `?embed#${slide}`;
  }, []);

  // Function to navigate to a specific slide
  const navigateToSlide = useCallback((slideNumber: number) => {
    console.log(`Navigating to slide ${slideNumber}`);
    
    // Only update if the slide number has changed
    if (currentSlide !== slideNumber && quiz.url) {
      // Update URL with the new slide number without triggering a navigation
      const url = new URL(window.location.href);
      url.searchParams.set('slide', slideNumber.toString());
      window.history.pushState({}, '', url.toString());
      
      // Check if iframe already exists
      if (iframeContainerRef.current) {
        const existingIframe = iframeContainerRef.current.querySelector('iframe');
        
        if (existingIframe) {
          try {
            // First attempt: Try to use the Canva API to navigate without reload
            // This uses postMessage which is more reliable than changing the src
            existingIframe.contentWindow?.postMessage(
              { type: 'showSlide', slideNumber: slideNumber },
              '*'  // Target origin - using * for simplicity but can be restricted to Canva domain
            );
            
            // Backup method: Update the hash in the URL (in case the postMessage approach doesn't work)
            const currentSrc = existingIframe.src;
            const baseUrl = currentSrc.split('#')[0]; // Get the part before the hash
            
            // Use location.replace to avoid adding to browser history
            if (existingIframe.contentWindow?.location) {
              existingIframe.contentWindow.location.replace(`${baseUrl}#${slideNumber}`);
            } else {
              // Fallback to changing src if needed
              existingIframe.src = `${baseUrl}#${slideNumber}`;
            }
          } catch (e) {
            console.warn('Error navigating iframe, falling back to src change', e);
            // Final fallback: just change the src
            const currentSrc = existingIframe.src;
            const baseUrl = currentSrc.split('#')[0];
            existingIframe.src = `${baseUrl}#${slideNumber}`;
          }
        } else {
          // If no iframe exists yet, create a new one
          // Clear the container
          iframeContainerRef.current.innerHTML = '';
          
          // Create a new iframe
          const iframe = document.createElement('iframe');
          iframe.src = getEmbedUrl(quiz.url, slideNumber);
          iframe.style.position = 'absolute';
          iframe.style.width = '100%';
          iframe.style.height = '100%';
          iframe.style.top = '0';
          iframe.style.left = '0';
          iframe.style.border = 'none';
          iframe.style.padding = '0';
          iframe.style.margin = '0';
          iframe.setAttribute('allowfullscreen', 'true');
          
          // Add the iframe to the container
          iframeContainerRef.current.appendChild(iframe);
        }
      }
    }
  }, [currentSlide, quiz.url, getEmbedUrl]);

  // Function to navigate to a specific question - now just passes the slide number directly
  const navigateToSlideFromLatestAnswers = useCallback((slideNumber: number) => {
    // Since we're now tracking slides directly, we don't need to convert question to slide
    // Just navigate to the slide directly
    navigateToSlide(slideNumber);
  }, [navigateToSlide]);

  // Effect to update the iframe when the currentSlide changes due to URL changes
  useEffect(() => {
    if (showQuiz && iframeContainerRef.current) {
      const existingIframe = iframeContainerRef.current.querySelector('iframe');
      if (existingIframe) {
        try {
          // Try to use the Canva API to navigate
          existingIframe.contentWindow?.postMessage(
            { type: 'showSlide', slideNumber: currentSlide },
            '*'
          );
          
          // Backup method: Update the hash in the URL
          const currentSrc = existingIframe.src;
          const baseUrl = currentSrc.split('#')[0];
          
          if (existingIframe.contentWindow?.location) {
            existingIframe.contentWindow.location.replace(`${baseUrl}#${currentSlide}`);
          } else {
            existingIframe.src = `${baseUrl}#${currentSlide}`;
          }
        } catch (e) {
          console.warn('Error updating iframe on URL change', e);
          const currentSrc = existingIframe.src;
          const baseUrl = currentSrc.split('#')[0];
          existingIframe.src = `${baseUrl}#${currentSlide}`;
        }
      }
    }
  }, [currentSlide, showQuiz]);

  // Create the initial iframe when the component mounts or when showQuiz changes
  useEffect(() => {
    if (showQuiz && quiz.url && iframeContainerRef.current) {
      // Check if iframe already exists
      const existingIframe = iframeContainerRef.current.querySelector('iframe');
      
      if (!existingIframe) {
        // Clear the container
        iframeContainerRef.current.innerHTML = '';
        
        // Create a new iframe
        const iframe = document.createElement('iframe');
        iframe.src = getEmbedUrl(quiz.url, currentSlide);
        iframe.style.position = 'absolute';
        iframe.style.width = '100%';
        iframe.style.height = '100%';
        iframe.style.top = '0';
        iframe.style.left = '0';
        iframe.style.border = 'none';
        iframe.style.padding = '0';
        iframe.style.margin = '0';
        iframe.setAttribute('allowfullscreen', 'true');
        
        // Add the iframe to the container
        iframeContainerRef.current.appendChild(iframe);
      }
    }
  }, [showQuiz, quiz.url, getEmbedUrl, currentSlide]);

  // Add message listener for communication with the Canva iframe
  useEffect(() => {
    const handleIframeMessages = (event: MessageEvent) => {
      // Check if the message is from Canva
      if (event.data && typeof event.data === 'object') {
        console.log('Received message from iframe:', event.data);
        
        // Handle any responses from Canva if needed
        if (event.data.type === 'slideChanged') {
          console.log('Slide changed in Canva to:', event.data.slideNumber);
          // Could update UI state based on this if needed
        }
      }
    };
    
    // Add the event listener
    window.addEventListener('message', handleIframeMessages);
    
    // Clean up the event listener when component unmounts
    return () => {
      window.removeEventListener('message', handleIframeMessages);
    };
  }, []);

  const scoreboard = Object.entries(teamTotals)
    .map(([team, rounds]) => {
      const total = Object.values(rounds).reduce((acc, val) => acc + val, 0);
      return { team, total, ...rounds };
    })
    .sort((a, b) => b.total - a.total);

  // Function to navigate to next/previous slides including special slides
  const goToNextSlide = useCallback(() => {
    navigateToSlide(currentSlide + 1);
  }, [currentSlide, navigateToSlide]);

  const goToPrevSlide = useCallback(() => {
    if (currentSlide > 1) {
      navigateToSlide(currentSlide - 1);
    }
  }, [currentSlide, navigateToSlide]);

  // Generate the quiz URL with path parameter instead of query parameter
  const quizUrl = `${window.location.origin}/quiz/${quiz.id}`;

  return (
    <div className="max-w-full overflow-x-auto p-0">
      {!quiz.url ? (
        <div className="flex flex-col items-center justify-center gap-8 my-12">
          <div className="max-w-xl w-full p-8 bg-white rounded-lg shadow-md border-2 border-dashed border-amber-300 flex flex-col items-center">
            <div className="text-center mb-6 w-full flex flex-col items-center">
              <h3 className="text-xl font-semibold text-gray-800 mb-3">Липсва URL към презентацията</h3>
              <p className="text-gray-600 mb-6">
                За да активирате QR кода и бутона за стартиране на куиза, моля добавете URL към Canva презентацията.
              </p>
              <button
                onClick={() => {
                  // Find the edit button in the header and click it
                  const editButton = document.querySelector('button[class*="btn-ghost"]') as HTMLButtonElement;
                  if (editButton) editButton.click();
                }}
                className="min-w-[200px] px-6 py-3 text-lg font-semibold text-white bg-gradient-to-r from-amber-500 to-orange-500 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 hover:from-amber-400 hover:to-orange-400 flex items-center justify-center gap-2"
              >
                <FiEdit2 className="w-4 h-4" />
                Добави URL
              </button>
            </div>
            <div className="bg-amber-50 p-4 rounded-lg w-full max-w-md">
              <h4 className="font-medium text-amber-800 mb-2">Защо е необходим URL:</h4>
              <ul className="list-disc list-inside space-y-1 text-amber-700 text-sm">
                <li>URL-ът е необходим за показване на въпросите от презентацията</li>
                <li>Без URL не може да се генерира QR код за участниците</li>
                <li>Без URL не може да се стартира куизът</li>
              </ul>
            </div>
          </div>
        </div>
      ) : !showQuiz ? (
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 min-h-[70vh]">
          {/* Left side - Bigger QR code */}
          <div className="flex flex-col items-center justify-center">
            <div className="p-6 bg-white rounded-lg shadow-md">
              <QRCode 
                value={quizUrl} 
                size={450}
                logoImage="/logo.png"
                logoWidth={150}
                logoHeight={150}
                qrStyle="squares"
                quietZone={10}
                ecLevel="H"
                removeQrCodeBehindLogo={true}
                logoPadding={2}
                eyeRadius={10}
              />
            </div>
            <div className="text-center mt-6">
              <p className="text-lg text-gray-700 mb-2">Сканирайте QR кода, за да се присъедините към куиза</p>
              <p className="text-sm text-gray-500 mb-6">{quizUrl}</p>
              <button
                onClick={() => {
                  setShowQuiz(true);
                  navigateToSlide(1);
                }}
                className="min-w-[140px] px-6 py-3 text-lg font-semibold text-white bg-gradient-to-r from-amber-500 to-orange-500 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 hover:from-amber-400 hover:to-orange-400"
              >
                <span className="flex items-center justify-center gap-2">
                  <FiPlay className="w-4 h-4" />
                  Започни куиза
                </span>
              </button>
            </div>
          </div>
          
          {/* Right side - Teams that have joined */}
          <div className="flex flex-col justify-start">
            <OnlineTeams />
          </div>
        </div>
      ) : (
        <div className="flex flex-col gap-4">
          {/* Two-column layout for quiz content with 4/5 - 1/5 split on desktop, stacked on mobile */}
          <div className="grid grid-cols-1 lg:grid-cols-5 gap-1">
            {/* Left column: Presentation iframe (4/5 width on desktop, full width on mobile) */}
            <div className="lg:col-span-4 lg:pr-1">
              <div style={{
                position: 'relative',
                width: '100%',
                height: 0,
                paddingTop: '56.25%',
                paddingBottom: 0,
                boxShadow: '0 2px 8px 0 rgba(63,69,81,0.16)',
                marginTop: '0',
                marginBottom: '0.25em',
                overflow: 'hidden',
                borderRadius: '8px',
                willChange: 'transform'
              }} ref={iframeContainerRef}>
                {/* Initial iframe will be created by useEffect */}
              </div>
              
              {/* Navigation controls beneath the presentation */}
              <div className="flex justify-center space-x-4 mb-1">
                <button
                  onClick={goToPrevSlide}
                  disabled={currentSlide <= 1}
                  className={`px-3 py-2 rounded-md transition-colors flex items-center gap-2 ${
                    currentSlide <= 1
                      ? 'bg-gray-200 text-gray-400 cursor-not-allowed'
                      : 'bg-amber-100 hover:bg-amber-200 text-amber-800'
                  }`}
                  title="Previous Slide"
                >
                  <FaArrowLeft /> Назад
                </button>
                <span className="flex items-center text-gray-700">
                  Слайд {currentSlide}
                </span>
                <button
                  onClick={goToNextSlide}
                  disabled={currentSlide >= 44}
                  className={`px-3 py-2 ${
                    currentSlide >= 44
                      ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
                      : 'bg-amber-500 hover:bg-amber-600 text-white'
                  } rounded-md transition-colors flex items-center gap-2`}
                  title="Next Slide"
                >
                  Напред <FaArrowRight />
                </button>
              </div>
              
              {/* Online teams and scoreboard below the presentation */}
              <div className="grid grid-cols-1 gap-1 mb-2 lg:hidden">
                <OnlineTeams />
                <Scoreboard scoreboard={scoreboard} />
              </div>
            </div>
            
            {/* Right column: Latest answers (1/5 width on desktop, full width on mobile) */}
            <div className="lg:col-span-1 lg:sticky lg:top-0 lg:h-screen lg:overflow-y-auto pb-4">
              <LatestAnswers 
                answers={answers} 
                onQuestionChange={navigateToSlideFromLatestAnswers} 
                currentQuestionNumber={currentQuestionNumber}
              />
            </div>
          </div>
          
          {/* Teams and scoreboard below the presentation on desktop */}
          <div className="hidden lg:grid lg:grid-cols-2 gap-2 mb-2">
            <OnlineTeams />
            <Scoreboard scoreboard={scoreboard} />
          </div>
          
          {/* Round tables below both columns */}
          <div className="mt-2">
            {Object.entries(groupedAnswers).map(([roundLabel, answers]) => (
              <RoundTable key={roundLabel} roundLabel={roundLabel} answers={answers} />
            ))}
          </div>
          
          <div className="h-4"></div>
        </div>
      )}
    </div>
  );
}