import React, { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { RoundDialog } from './RoundDialog';
import { AnswersHistory } from './AnswersHistory';
import { QuizInput } from './QuizInput';
import {
FaPaperPlane,
FaHistory,
FaHourglassHalf,
FaBell,
FaCheck,
FaUndo
} from 'react-icons/fa';
import { createClient } from '@/utils/supabase/client';
import {
getCurrentRound,
questionToRoundQuestion
} from '../../utils/quizHelpers';
import ReactConfetti from 'react-confetti';
type QuizQuestionProps = {
teamName: string;
initialQuestionNumber: number;
quizId?: number | null;
};
export default function QuizQuestion({
teamName,
initialQuestionNumber,
quizId
}: QuizQuestionProps) {
const [showHistory, setShowHistory] = useState(false);
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
const [countdown, setCountdown] = useState(0);
const [notificationMessage, setNotificationMessage] = useState('');
const [showNotification, setShowNotification] = useState(false);
const [isSynced, setIsSynced] = useState(false);
const [showConfetti, setShowConfetti] = useState(false);
const [windowDimensions, setWindowDimensions] = useState({ width: 0, height: 0 });
const submitButtonRef = useRef<HTMLButtonElement>(null);
const [confettiPosition, setConfettiPosition] = useState({ x: 0, y: 0 });
// Quiz state
const [questionNumber, setQuestionNumber] = useState(initialQuestionNumber);
const [currentRound, setCurrentRound] = useState(getCurrentRound(initialQuestionNumber));
const [answer, setAnswer] = useState('');
const [answersHistory, setAnswersHistory] = useState<
{ question: number; round: number; answer: string }[]
>([]);
const [adminCurrentQuestion, setAdminCurrentQuestion] = useState<
number | null
>(null);
const [errorMessage, setErrorMessage] = useState('');
const supabase = createClient();
// Dialog state
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dialogType, setDialogType] = useState<'round' | 'question'>('round');
// Check if current question has been answered
const hasAnsweredCurrentQuestion = answersHistory.some(
(ans) => ans.question === questionNumber
);
// Get the answer for the current question if it exists
const currentQuestionAnswer = answersHistory.find(
(ans) => ans.question === questionNumber
);
// Listen for the admin's current question/slide
useEffect(() => {
// Only set up the admin broadcast channel if we have a valid quizId
if (!quizId) return;
const channel = supabase.channel('admin-broadcast');
let syncAttempts = 0;
const maxSyncAttempts = 5;
let syncTimer: ReturnType<typeof setTimeout>;
let hasReceivedInitialSync = false;
// Handle broadcast messages from admin about slide changes
channel.on('broadcast', { event: 'slide-change' }, (payload) => {
// Only process messages for our quiz
if (
payload.payload.quizId === quizId &&
payload.payload.currentQuestionNumber !== null
) {
const newQuestionNumber = payload.payload.currentQuestionNumber;
setAdminCurrentQuestion(newQuestionNumber);
hasReceivedInitialSync = true; // Mark that we've received sync data
setIsSynced(true);
// Always notify about question changes since we're always synced
if (newQuestionNumber !== questionNumber) {
const currentRound = getCurrentRound(newQuestionNumber);
const questionInRound = questionToRoundQuestion(newQuestionNumber);
setNotificationMessage(
`Презентаторът премина към Кръг ${currentRound}, Въпрос ${questionInRound}`
);
setShowNotification(true);
// Auto-hide notification after 5 seconds
setTimeout(() => {
setShowNotification(false);
}, 5000);
}
}
});
// Handle response to our sync request
channel.on('broadcast', { event: 'slide-current-state' }, (payload) => {
if (
payload.payload.quizId === quizId &&
payload.payload.currentQuestionNumber !== null
) {
console.log("Received slide state:", payload.payload);
setAdminCurrentQuestion(payload.payload.currentQuestionNumber);
hasReceivedInitialSync = true; // Mark that we've received sync data
setIsSynced(true);
// Clear any pending retry timer since we got a response
if (syncTimer) {
clearTimeout(syncTimer);
}
}
});
const requestCurrentSlideState = async () => {
if (!hasReceivedInitialSync && syncAttempts < maxSyncAttempts) {
console.log(`Requesting current slide state (attempt ${syncAttempts + 1}/${maxSyncAttempts})...`);
try {
await channel.send({
type: 'broadcast',
event: 'request-slide-state',
payload: {
quizId,
teamName
}
});
syncAttempts++;
// Schedule another attempt if we still haven't received a response
syncTimer = setTimeout(() => {
if (!hasReceivedInitialSync) {
requestCurrentSlideState();
}
}, 2000); // Retry every 2 seconds
} catch (error) {
console.error("Error requesting slide state:", error);
setIsSynced(false);
// Still retry on error
syncTimer = setTimeout(() => {
if (!hasReceivedInitialSync) {
requestCurrentSlideState();
}
}, 2000);
}
} else if (syncAttempts >= maxSyncAttempts && !hasReceivedInitialSync) {
setIsSynced(false);
}
};
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Once subscribed, request the current slide state from admin
requestCurrentSlideState();
}
});
return () => {
// Clear any pending timers
if (syncTimer) {
clearTimeout(syncTimer);
}
supabase.removeChannel(channel);
};
}, [quizId, supabase, questionNumber, teamName]);
// Load saved answers from localStorage
useEffect(() => {
const savedAnswers = localStorage.getItem('quizAnswers');
if (savedAnswers) {
const answers = JSON.parse(savedAnswers);
// Only load if it's the same team
if (answers.length > 0 && answers[0].teamName === teamName) {
setAnswersHistory(
answers.map(({ question, round, answer }: any) => ({
question,
round,
answer
}))
);
}
}
}, [teamName]);
// Always sync with admin question whenever it changes
useEffect(() => {
if (adminCurrentQuestion !== null) {
setQuestionNumber(adminCurrentQuestion);
}
}, [adminCurrentQuestion]);
// Update current round whenever question number changes
useEffect(() => {
const newRound = getCurrentRound(questionNumber);
setCurrentRound(newRound);
}, [questionNumber]);
// Save progress to localStorage whenever it changes
useEffect(() => {
localStorage.setItem(
'quizTeamData',
JSON.stringify({
name: teamName,
questionNumber,
quizId
})
);
}, [teamName, questionNumber, quizId]);
// Save answers to localStorage whenever they change
useEffect(() => {
localStorage.setItem(
'quizAnswers',
JSON.stringify(
answersHistory.map((answer) => ({
...answer,
teamName, // Add teamName to identify which team's answers these are
quizId // Add quizId to identify which quiz these answers belong to
}))
)
);
}, [answersHistory, teamName, quizId]);
/**
* Countdown effect for button disable.
*/
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
if (isButtonDisabled && countdown > 0) {
timer = setTimeout(() => setCountdown(countdown - 1), 1000);
} else if (countdown === 0) {
setIsButtonDisabled(false);
}
return () => clearTimeout(timer);
}, [isButtonDisabled, countdown]);
// Handle cleanup when team leaves
useEffect(() => {
const channel = supabase.channel('online_teams');
let isActive = true;
// Function to update team status
const updateTeamStatus = async (active: boolean) => {
if (active) {
// Set team as active
await channel.track({
team: teamName.trim(),
online_at: new Date().toISOString(),
status: 'active'
});
} else {
// Set team as inactive
await channel.track({
team: teamName.trim(),
online_at: new Date().toISOString(),
status: 'inactive'
});
}
};
// Handle page visibility change
const handleVisibilityChange = () => {
isActive = !document.hidden;
updateTeamStatus(isActive);
};
// Handle page unload/close
const handleBeforeUnload = () => {
channel.untrack();
channel.unsubscribe();
};
// Initial status update
updateTeamStatus(true);
// Add event listeners
window.addEventListener('beforeunload', handleBeforeUnload);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
document.removeEventListener('visibilitychange', handleVisibilityChange);
handleBeforeUnload();
};
}, [teamName, supabase]);
// Set window dimensions after mounting
useEffect(() => {
setWindowDimensions({
width: window.innerWidth,
height: window.innerHeight
});
const handleResize = () => {
setWindowDimensions({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
/**
* Submit quiz answer.
*/
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (isButtonDisabled || answer.trim() === '') return;
setIsButtonDisabled(true);
setCountdown(1);
// Get the position of the submit button for the confetti origin
if (submitButtonRef.current) {
const rect = submitButtonRef.current.getBoundingClientRect();
setConfettiPosition({
x: rect.left + rect.width / 2,
y: rect.top
});
}
try {
const response = await fetch('/api/quiz', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
teamName,
answer,
questionNumber,
quizId: quizId ? Number(quizId) : null
})
});
if (response.ok) {
const newAnswer = {
question: questionNumber,
round: currentRound,
answer
};
setAnswersHistory((prev) => [...prev, newAnswer]);
setAnswer('');
// Show confetti on successful submission
setShowConfetti(true);
// Hide confetti after 3 seconds
setTimeout(() => {
setShowConfetti(false);
}, 3000);
} else {
const data = await response.json();
alert(`Грешка: ${data.message}`);
}
} catch (error) {
alert(
'Възникна грешка при изпращането на отговора. Моля, опитайте отново.'
);
}
};
/**
* Reset team data and redirect to team setup
*/
const handleReset = () => {
// Remove team data and answers from localStorage
localStorage.removeItem('quizTeamData');
localStorage.removeItem('quizAnswers');
// Redirect to the quiz page
window.location.href = quizId ? `/quiz/${quizId}` : '/quiz';
};
return (
<div className="relative">
{/* Confetti effect */}
{showConfetti && windowDimensions.width > 0 && (
<ReactConfetti
width={windowDimensions.width}
height={windowDimensions.height}
recycle={false}
numberOfPieces={200}
confettiSource={{
x: confettiPosition.x,
y: confettiPosition.y,
w: 10,
h: 10
}}
initialVelocityY={10}
gravity={0.3}
/>
)}
{/* Notification */}
<AnimatePresence>
{showNotification && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 bg-green-100 border border-green-300 text-green-800 px-4 py-3 rounded-lg shadow-lg flex items-center gap-2"
>
<FaBell className="text-green-600" />
{notificationMessage}
<button
onClick={() => setShowNotification(false)}
className="ml-3 text-green-700 hover:text-green-900"
>
×
</button>
</motion.div>
)}
</AnimatePresence>
{/* Round Dialog */}
<AnimatePresence>
{isDialogOpen && (
<RoundDialog
dialogType={dialogType}
currentRound={currentRound}
setIsDialogOpen={setIsDialogOpen}
setQuestionNumber={setQuestionNumber}
/>
)}
</AnimatePresence>
{/* History Dialog */}
<AnimatePresence>
{showHistory && (
<AnswersHistory
answersHistory={answersHistory}
setShowHistory={setShowHistory}
/>
)}
</AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="px-6 py-8 bg-amber-50/70 backdrop-blur-sm rounded-xl shadow-sm border border-amber-100/50 relative overflow-hidden"
>
<motion.div
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2 }}
>
{/* Header Bar - All elements on one line */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 mb-6">
{/* Left side: Team name and question info */}
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto">
<div className="px-4 py-1.5 bg-gradient-to-r from-amber-500 to-orange-500 text-white font-medium rounded-lg shadow-sm">
{teamName}
</div>
<div className="text-base sm:text-lg font-bold text-amber-900">
Кръг {currentRound}, Въпрос{' '}
{questionToRoundQuestion(questionNumber)}
</div>
</div>
{/* Right side: Sync status and history button */}
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
{/* Subtle pulsating dot for sync status */}
<div
className="relative flex items-center"
title="Автоматично синхронизиран с презентатора"
>
<div className={`
w-4 h-4 rounded-full
${isSynced ? 'bg-green-500' : 'bg-red-500'}
animate-pulse
`}></div>
</div>
<button
onClick={() => setShowHistory(true)}
className="flex items-center gap-1 px-3 py-1.5 bg-amber-100 text-amber-700 rounded-lg text-sm font-medium hover:bg-amber-200 transition-colors"
title="История на отговорите"
>
<FaHistory size={14} />
История
</button>
</div>
</div>
{/* Quiz Form */}
<form onSubmit={handleSubmit}>
<QuizInput
currentRound={currentRound}
answer={
hasAnsweredCurrentQuestion
? currentQuestionAnswer?.answer || ''
: answer
}
setAnswer={setAnswer}
disabled={hasAnsweredCurrentQuestion || isButtonDisabled}
readOnly={hasAnsweredCurrentQuestion}
/>
<div className="flex items-center justify-center gap-4 mt-6">
<motion.button
ref={submitButtonRef}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
type="submit"
disabled={
hasAnsweredCurrentQuestion || isButtonDisabled || !answer
}
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 ${
hasAnsweredCurrentQuestion || isButtonDisabled || !answer
? 'opacity-50 cursor-not-allowed'
: 'hover:from-amber-400 hover:to-orange-400'
}`}
>
{isButtonDisabled ? (
<span className="flex items-center gap-2">
<FaHourglassHalf className="text-lg animate-pulse" />
{countdown}сек
</span>
) : hasAnsweredCurrentQuestion ? (
<span className="flex items-center gap-2">
<FaCheck className="text-lg" />
Отговорено
</span>
) : (
<span className="flex items-center gap-2">
<FaPaperPlane className="text-lg" />
Изпрати
</span>
)}
</motion.button>
</div>
<div className="flex justify-center mt-6">
<motion.button
type="button"
onClick={handleReset}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-sm px-3 py-2 text-sm text-amber-800 bg-amber-100 border border-amber-200 rounded-lg hover:bg-amber-200 transition-colors duration-200"
>
<span className="flex items-center gap-1">
<FaUndo className="text-xs" />
Рестартирай връзката
</span>
</motion.button>
</div>
</form>
</motion.div>
</motion.div>
</div>
);
}