·10 min

Complete Guide: Building a High-Performance Quiz Platform with Next.js and Firebase Integration

A comprehensive guide to building a secure, high-performance quiz platform with Next.js and Firebase, featuring authentication, real-time scoring, iframe isolation, and admin management capabilities.

DK

Daniel Kliewer

Author, Sovereign AI

Next.jsFirebaseQuiz PlatformAuthenticationFirestoreReal-TimeIframe SecurityAdmin DashboardEducational TechnologyWeb Development
Sovereign AI book cover

From the Book

This is from Sovereign AI: Building Local-First Intelligent Systems.

Get the Book — $88
Complete Guide: Building a High-Performance Quiz Platform with Next.js and Firebase Integration

Image

Building a High-Performance Quiz Platform with Next.js and Firebase

Creating an online quiz platform can be challenging, especially when you need to handle user authentication, store scores, and display dynamic content. In this comprehensive guide, I'll walk you through how to optimize a Next.js quiz application that leverages Firebase for authentication and Firestore for data storage.

The Challenge of Quiz Applications

Many educational platforms struggle with performance issues, security vulnerabilities, and code maintainability when implementing quiz functionality. Whether you're building a learning management system, an educational app, or just a fun quiz site, these challenges can significantly impact user experience.

Let's explore how to streamline a Next.js quiz platform with Firebase integration to create a secure, fast, and maintainable solution.

1. Centralizing Firebase Initialization

One common mistake is initializing Firebase multiple times across different components. This not only affects performance but can also lead to unexpected behaviors.

The Solution: Single Firebase Instance

Create a dedicated firebase.js file to handle initialization once:

javascript
1// firebase.js
2import { initializeApp } from 'firebase/app';
3import { getAuth } from 'firebase/auth';
4import { getFirestore } from 'firebase/firestore';
5
6const firebaseConfig = {
7 apiKey: 'YOUR_KEY',
8 authDomain: 'your-app.firebaseapp.com',
9 projectId: 'your-app',
10 storageBucket: 'your-app.appspot.com',
11 messagingSenderId: '123456789',
12 appId: '1:123456789:web:abcdef123456789'
13};
14
15// Initialize Firebase only once
16const app = initializeApp(firebaseConfig);
17const auth = getAuth(app);
18const db = getFirestore(app);
19
20export { auth, db };

By exporting the initialized auth and db instances, you can import them wherever needed without creating redundant Firebase connections.

2. Optimizing the Quiz Page Component

Your quiz page should efficiently handle quiz rendering and score saving without unnecessary re-renders or network calls.

Implementation Approach #1: Direct HTML Rendering

javascript
1// pages/quiz/[id].js
2import { useEffect } from 'react';
3import { useRouter } from 'next/router';
4import { doc, setDoc } from 'firebase/firestore';
5import { auth, db } from '../../firebase';
6
7const QuizPage = ({ quizHtml }) => {
8 const router = useRouter();
9 const { id } = router.query;
10
11 useEffect(() => {
12 // Expose the saveScore function to the quiz content
13 const saveScore = async (score) => {
14 const user = auth.currentUser;
15 if (user) {
16 const userDoc = doc(db, 'grades', user.uid);
17 await setDoc(
18 userDoc,
19 { [id]: { score, updatedAt: new Date() } },
20 { merge: true }
21 );
22 }
23 };
24
25 window.saveScore = saveScore;
26 }, [id]);
27
28 return (
29 <div>
30 <div dangerouslySetInnerHTML={{ __html: quizHtml }} />
31 </div>
32 );
33};
34
35export async function getStaticProps({ params }) {
36 const quizHtml = await getQuizHTML(params.id); // Implement this function to fetch quiz HTML
37 return { props: { quizHtml } };
38}
39
40export async function getStaticPaths() {
41 // Implement this function to generate paths for all quizzes
42 return {
43 paths: [
44 { params: { id: 'quiz1' } },
45 { params: { id: 'quiz2' } },
46 // Add more quizzes as needed
47 ],
48 fallback: false
49 };
50}
51
52export default QuizPage;

This approach works but has potential security risks due to the use of dangerouslySetInnerHTML.

3. Enhancing Security with Iframe Isolation

A more secure approach is to isolate quiz content within an iframe, preventing potential XSS attacks and providing better content separation.

Implementation Approach #2: Iframe Isolation

javascript
1// pages/quiz/[id].js
2import { useEffect } from 'react';
3import { useRouter } from 'next/router';
4import { doc, setDoc } from 'firebase/firestore';
5import { auth, db } from '../../firebase';
6
7const QuizPage = ({ quizPath }) => {
8 const router = useRouter();
9 const { id } = router.query;
10
11 useEffect(() => {
12 const saveScore = async (score) => {
13 const user = auth.currentUser;
14 if (user) {
15 const userDoc = doc(db, 'grades', user.uid);
16 await setDoc(
17 userDoc,
18 { [id]: { score, updatedAt: new Date() } },
19 { merge: true }
20 );
21 } else {
22 // Handle unauthenticated user scenario
23 console.log('User not authenticated. Score not saved.');
24 router.push('/login?returnUrl=' + router.asPath);
25 }
26 };
27
28 // Listen for messages from the iframe
29 window.addEventListener('message', (event) => {
30 if (event.data.type === 'saveScore') {
31 saveScore(event.data.score);
32 }
33 });
34
35 // Cleanup event listener
36 return () => {
37 window.removeEventListener('message', (event) => {
38 if (event.data.type === 'saveScore') {
39 saveScore(event.data.score);
40 }
41 });
42 };
43 }, [id, router]);
44
45 return (
46 <div className="quiz-container">
47 <h1>Quiz {id}</h1>
48 <iframe
49 src={quizPath}
50 width="100%"
51 height="600px"
52 style={{ border: 'none' }}
53 title={`Quiz ${id}`}
54 />
55 </div>
56 );
57};
58
59export async function getStaticProps({ params }) {
60 const quizPath = `/quizzes/${params.id}.html`; // Path to quiz HTML files in public directory
61 return { props: { quizPath } };
62}
63
64export async function getStaticPaths() {
65 // Generate paths for all quizzes
66 return {
67 paths: [
68 { params: { id: 'quiz1' } },
69 { params: { id: 'quiz2' } },
70 // Add more quizzes as needed
71 ],
72 fallback: false
73 };
74}
75
76export default QuizPage;

To make this approach work, your quiz HTML files (stored in the public/quizzes/ directory) should include code to communicate with the parent page:

html
1<!-- public/quizzes/quiz1.html -->
2<!DOCTYPE html>
3<html lang="en">
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <title>Quiz 1</title>
8 <style>
9 body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
10 .question { margin-bottom: 20px; }
11 button { padding: 10px 20px; background: #4285f4; color: white; border: none; border-radius: 4px; cursor: pointer; }
12 </style>
13</head>
14<body>
15 <h2>Science Quiz</h2>
16 <form id="quizForm" onsubmit="calculateScore(); return false;">
17 <div class="question">
18 <p>1. What is the chemical symbol for water?</p>
19 <input type="radio" name="q1" value="a" id="q1a">
20 <label for="q1a">O2</label><br>
21 <input type="radio" name="q1" value="b" id="q1b">
22 <label for="q1b">H2O</label><br>
23 <input type="radio" name="q1" value="c" id="q1c">
24 <label for="q1c">CO2</label>
25 </div>
26
27 <div class="question">
28 <p>2. Which planet is known as the Red Planet?</p>
29 <input type="radio" name="q2" value="a" id="q2a">
30 <label for="q2a">Venus</label><br>
31 <input type="radio" name="q2" value="b" id="q2b">
32 <label for="q2b">Mars</label><br>
33 <input type="radio" name="q2" value="c" id="q2c">
34 <label for="q2c">Jupiter</label>
35 </div>
36
37 <button type="submit">Submit Quiz</button>
38 </form>
39
40 <script>
41 function calculateScore() {
42 const form = document.getElementById('quizForm');
43 let score = 0;
44 const answers = {
45 q1: 'b', // H2O
46 q2: 'b' // Mars
47 };
48
49 // Check each question
50 for (const [question, correctAnswer] of Object.entries(answers)) {
51 const selectedValue = form.elements[question].value;
52 if (selectedValue === correctAnswer) {
53 score += 1;
54 }
55 }
56
57 const totalQuestions = Object.keys(answers).length;
58 const percentage = Math.round((score / totalQuestions) * 100);
59
60 // Send score to parent page
61 window.parent.postMessage({ type: 'saveScore', score: percentage }, '*');
62
63 // Show results to user
64 alert(`You scored ${percentage}% (${score}/${totalQuestions} correct)`);
65 }
66 </script>
67</body>
68</html>

4. Security Considerations

Even with the iframe approach, there are additional security measures to consider:

Content Security Policy (CSP)

Set up a proper Content Security Policy in your Next.js application:

javascript
1// next.config.js
2module.exports = {
3 async headers() {
4 return [
5 {
6 source: '/(.*)',
7 headers: [
8 {
9 key: 'Content-Security-Policy',
10 value: "default-src 'self'; frame-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
11 }
12 ]
13 }
14 ]
15 }
16}

HTML Sanitization

If you're using the direct HTML rendering approach, always sanitize any external HTML content:

javascript
1import DOMPurify from 'dompurify';
2
3// In your component
4const sanitizedHtml = DOMPurify.sanitize(quizHtml);
5
6return (
7 <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
8);

5. Enhancing Your Quiz Platform

To create a truly robust quiz platform, consider these additional features:

Authentication UI

Integrate Firebase Authentication UI for a seamless login experience:

javascript
1// components/AuthUI.js
2import { useState, useEffect } from 'react';
3import { onAuthStateChanged, signOut } from 'firebase/auth';
4import { auth } from '../firebase';
5
6const AuthUI = () => {
7 const [user, setUser] = useState(null);
8 const [loading, setLoading] = useState(true);
9
10 useEffect(() => {
11 const unsubscribe = onAuthStateChanged(auth, (user) => {
12 setUser(user);
13 setLoading(false);
14 });
15
16 return unsubscribe;
17 }, []);
18
19 const handleSignOut = async () => {
20 try {
21 await signOut(auth);
22 } catch (error) {
23 console.error('Error signing out:', error);
24 }
25 };
26
27 if (loading) return <div>Loading...</div>;
28
29 return (
30 <div className="auth-ui">
31 {user ? (
32 <div>
33 <p>Welcome, {user.displayName || user.email}</p>
34 <button onClick={handleSignOut}>Sign Out</button>
35 </div>
36 ) : (
37 <button onClick={() => window.location.href = '/login'}>Sign In</button>
38 )}
39 </div>
40 );
41};
42
43export default AuthUI;

Score Dashboard

Create a dashboard to display users' quiz scores:

javascript
1// pages/dashboard.js
2import { useState, useEffect } from 'react';
3import { doc, getDoc } from 'firebase/firestore';
4import { onAuthStateChanged } from 'firebase/auth';
5import { auth, db } from '../firebase';
6
7const Dashboard = () => {
8 const [scores, setScores] = useState({});
9 const [loading, setLoading] = useState(true);
10 const [user, setUser] = useState(null);
11
12 useEffect(() => {
13 const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
14 setUser(currentUser);
15
16 if (currentUser) {
17 try {
18 const userDocRef = doc(db, 'grades', currentUser.uid);
19 const userDoc = await getDoc(userDocRef);
20
21 if (userDoc.exists()) {
22 setScores(userDoc.data());
23 }
24 } catch (error) {
25 console.error('Error fetching scores:', error);
26 } finally {
27 setLoading(false);
28 }
29 } else {
30 setLoading(false);
31 }
32 });
33
34 return unsubscribe;
35 }, []);
36
37 if (loading) return <div>Loading your scores...</div>;
38
39 if (!user) return <div>Please log in to view your dashboard</div>;
40
41 return (
42 <div className="dashboard">
43 <h1>Your Quiz Scores</h1>
44
45 {Object.keys(scores).length > 0 ? (
46 <ul className="scores-list">
47 {Object.entries(scores).map(([quizId, data]) => (
48 <li key={quizId} className="score-item">
49 <div className="quiz-name">Quiz: {quizId}</div>
50 <div className="quiz-score">Score: {data.score}%</div>
51 <div className="quiz-date">
52 Completed: {new Date(data.updatedAt.toDate()).toLocaleString()}
53 </div>
54 </li>
55 ))}
56 </ul>
57 ) : (
58 <p>You haven't completed any quizzes yet.</p>
59 )}
60 </div>
61 );
62};
63
64export default Dashboard;

Admin Quiz Management

For educators or administrators, implement a quiz management system:

javascript
1// pages/admin/quizzes.js
2import { useState } from 'react';
3import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
4import { storage } from '../../firebase';
5
6const QuizManagement = () => {
7 const [file, setFile] = useState(null);
8 const [quizId, setQuizId] = useState('');
9 const [uploading, setUploading] = useState(false);
10 const [message, setMessage] = useState('');
11
12 const handleFileChange = (e) => {
13 if (e.target.files[0]) {
14 setFile(e.target.files[0]);
15 }
16 };
17
18 const handleUpload = async (e) => {
19 e.preventDefault();
20
21 if (!file || !quizId) {
22 setMessage('Please select a file and provide a quiz ID');
23 return;
24 }
25
26 setUploading(true);
27 setMessage('');
28
29 try {
30 // Upload to Firebase Storage
31 const storageRef = ref(storage, `quizzes/${quizId}.html`);
32 await uploadBytes(storageRef, file);
33 const downloadURL = await getDownloadURL(storageRef);
34
35 setMessage(`Quiz uploaded successfully! URL: ${downloadURL}`);
36 setFile(null);
37 setQuizId('');
38 } catch (error) {
39 console.error('Error uploading quiz:', error);
40 setMessage(`Error uploading quiz: ${error.message}`);
41 } finally {
42 setUploading(false);
43 }
44 };
45
46 return (
47 <div className="admin-panel">
48 <h1>Quiz Management</h1>
49
50 <form onSubmit={handleUpload} className="upload-form">
51 <div className="form-group">
52 <label htmlFor="quizId">Quiz ID:</label>
53 <input
54 type="text"
55 id="quizId"
56 value={quizId}
57 onChange={(e) => setQuizId(e.target.value)}
58 placeholder="e.g., science-quiz-1"
59 required
60 />
61 </div>
62
63 <div className="form-group">
64 <label htmlFor="quizFile">Quiz HTML File:</label>
65 <input
66 type="file"
67 id="quizFile"
68 accept=".html"
69 onChange={handleFileChange}
70 required
71 />
72 </div>
73
74 <button type="submit" disabled={uploading}>
75 {uploading ? 'Uploading...' : 'Upload Quiz'}
76 </button>
77 </form>
78
79 {message && <div className="message">{message}</div>}
80 </div>
81 );
82};
83
84export default QuizManagement;

Performance Optimization

To ensure your quiz platform runs smoothly, implement these additional optimizations:

  1. Implement caching: Use Firestore's offline capabilities to allow users to take quizzes without an internet connection.

  2. Lazy loading: Only load quiz content when necessary to reduce initial page load times.

  3. Server-side rendering for dashboard pages: Pre-render data-heavy pages to improve perceived performance.

  4. Implement analytics: Track quiz completion rates and user engagement to identify areas for improvement.

Conclusion

Building a high-performance quiz platform with Next.js and Firebase requires careful planning and implementation. By centralizing Firebase initialization, securing content with iframes or proper sanitization, and implementing additional features like user dashboards and admin panels, you can create a robust, maintainable quiz application.

The approaches outlined in this guide will help you avoid common pitfalls, enhance security, and provide a seamless experience for both students and educators. Whether you're building an educational platform, a corporate training tool, or just a fun quiz site, these techniques will help you create a professional-grade solution.

Remember that security should always be a priority when handling user data and displaying dynamic content. Regularly audit your code and stay updated with the latest security best practices for web applications.

Sovereign AI book cover

Sovereign AI: Building Local-First Intelligent Systems

by Daniel Kliewer · Paperback · 72 pages

The hands-on guide to building AI that runs on your hardware, keeps your data private, and eliminates cloud dependence. Working code included.