'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>
);
}