And… we’re back. Nothing says fun like a 3 week hiatus from slinging code. First checking to see were everything is. While I’ve been working on other things our consultants have been working on getting the awards system into D10.
Recent Updates Toggle Comment Threads | Keyboard Shortcuts
-
Elmer Masters
-
Elmer Masters
PR#53 reviewed and merged.
All of this will be deployed to d10dev.calidev.org on Monday so it should be available to show off at CALIcon25. -
Elmer Masters
Onto PR#53. More Awards and an update to Drupal core.
-
Elmer Masters
PR#52 merged into develop. Really need to fix datetime handling for LessonRun views
-
Elmer Masters
Resolving merge conflicts in PR#52
-
Elmer Masters
And PR#51 is history. It reminded me of the problem of seeing too much on a website. In the Awards system we have different views for different folks. Each set makes sense for the folks who use them. When you can see them all it looks chaotic and no fun at all. https://github.com/CCALI/drupal-10/pull/51
-
Elmer Masters
Back to the D10 PR thing. Currently reviewing the new event calendar and eLangdell book topic pages. Everything looks good. PR#45 is good to merge https://github.com/CCALI/drupal-10/pull/45
-
Elmer Masters
NextGen presentation layer for COLT
The idea is to create a modern presentation layer for COLT that has the features of the current Viewer but uses a different approach. Everything below was generated by Copilot using the following prompt where the XML was a 3 question QuizWright Quiz:
Create a react frontend for interactive quizzes that uses this XML format for the quiz and PHP backend with FrankenPHP and Mysql
Below is an example of how you might structure a project that uses a React‐based interactive quiz frontend with a PHP backend (using FrankenPHP) and MySQL for persistence. In this example the quiz content is stored as an XML file (whose format is similar to your sample) and is served by a simple PHP endpoint. The React app fetches that XML, parses it into questions and options, and lets the user answer the questions interactively. When the quiz is finished, the app can post the results back to the PHP backend, which in turn stores them in a MySQL database.
Note: You will need to adjust file paths, the XML parsing logic, and error checking to suit the full structure of your XML and your application needs. FrankenPHP allows you to run PHP “apps” very fast, so simply put your PHP API endpoints (e.g. in a folder called “api”) and let FrankenPHP handle routing based on your configuration.
Project Structure
A simple file/directory layout could be:
my-quiz-app/ ├── backend/ │ ├── api/ │ │ ├── getQuiz.php │ │ └── submit_result.php │ ├── quiz.xml │ └── db_config.php └── frontend/ ├── public/ │ └── index.html └── src/ ├── App.jsx ├── index.js └── components/ └── Quiz.jsx
Frontend (React)
Below is a simplified React component that fetches the quiz XML from the backend, parses it using the browser’s built-in DOMParser, and displays questions one by one. When the quiz is finished the app submits the score to the PHP backend.
src/components/Quiz.jsx
import React, { useEffect, useState } from 'react'; const Quiz = () => { const [quizData, setQuizData] = useState([]); // each element holds a parsed question const [currentIndex, setCurrentIndex] = useState(0); const [loading, setLoading] = useState(true); const [selectedAnswer, setSelectedAnswer] = useState(null); const [score, setScore] = useState(0); const [quizComplete, setQuizComplete] = useState(false); useEffect(() => { // Fetch the quiz XML from the backend endpoint fetch('/api/getQuiz.php') .then((response) => response.text()) .then((xmlString) => { // Use DOMParser to convert XML string into a document const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); // For this example we assume that questions are stored in <PAGE> elements // with an id like "Question 1", "Question 2", etc. const pages = Array.from(xmlDoc.getElementsByTagName('PAGE')); const questions = pages .filter((page) => { // Exclude pages used for content overview (e.g. "Contents" or other non-quiz pages) const id = page.getAttribute('ID'); return id && id.toLowerCase().indexOf('question') !== -1; }) .map((page) => { // Extract the question text from the <QUESTION> tag const questionTag = page.getElementsByTagName('QUESTION')[0]; const questionText = questionTag ? questionTag.textContent.trim() : 'No question text'; // Assume answer options are contained in <DETAIL> tags const details = Array.from(page.getElementsByTagName('DETAIL')).map((d) => d.textContent.trim() ); // Also capture feedback information from <FEEDBACK> tags (which in this XML signal correctness) const feedbackNodes = Array.from(page.getElementsByTagName('FEEDBACK')); const feedback = feedbackNodes.map((node) => ({ button: node.getAttribute('BUTTON'), detail: node.getAttribute('DETAIL'), grade: node.getAttribute('GRADE') })); return { id: page.getAttribute('ID'), questionText, options: details, feedback // simple feedback info, e.g. which option is RIGHT/WRONG }; }); setQuizData(questions); setLoading(false); }) .catch((error) => { console.error('Error fetching/parsing quiz XML:', error); setLoading(false); }); }, []); const handleAnswerSelect = (option) => { setSelectedAnswer(option); }; const handleNext = () => { if (selectedAnswer !== null) { // For demonstration, assume that if any feedback entry has grade RIGHT, // then the question is considered answered correctly. // In a real app you’d compare the selected answer to an answer key from the XML. const currentQuestion = quizData[currentIndex]; const isCorrect = currentQuestion.feedback.some((fb) => fb.grade === 'RIGHT'); if (isCorrect) { setScore((prev) => prev + 1); } // Reset for next question setSelectedAnswer(null); if (currentIndex + 1 < quizData.length) { setCurrentIndex(currentIndex + 1); } else { setQuizComplete(true); } } }; const submitResults = () => { // Submit the final score to the backend using the PHP endpoint fetch('/api/submit_result.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'Elmer', // or let the user provide a name score: score }) }) .then((response) => response.json()) .then((data) => { console.log('Submission success:', data); }) .catch((err) => console.error('Submission error: ', err)); }; if (loading) return <div>Loading quiz...</div>; if (quizComplete) return ( <div className="quiz-complete"> <h2>Quiz Complete!</h2> <p>Your score: {score} / {quizData.length}</p> <button onClick={submitResults}>Submit Results</button> </div> ); const currentQuestion = quizData[currentIndex]; return ( <div className="quiz"> <h2>{currentQuestion.questionText}</h2> <ul> {currentQuestion.options.map((option, idx) => ( <li key={idx}> <button onClick={() => handleAnswerSelect(option)} style={{ backgroundColor: selectedAnswer === option ? '#e0e0e0' : '#fff' }} > {option} </button> </li> ))} </ul> {selectedAnswer && <button onClick={handleNext}>Next</button>} </div> ); }; export default Quiz;
src/App.jsx
This is the main application file that renders our quiz component:
import React from 'react'; import Quiz from './components/Quiz'; function App() { return ( <div className="App"> <header> <h1>Interactive Quiz</h1> </header> <Quiz /> </div> ); } export default App;
src/index.js
This file bootstraps the React application:
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root'));
Tip: To make the XML parsing more robust you might choose to use a dedicated XML-to-JSON library. In addition, refining which pages count as questions (e.g. excluding a table of contents) will depend on your exact XML format.
Backend (PHP with FrankenPHP and MySQL)
Place these PHP files in your backend “api” folder. FrankenPHP will let you write PHP code that serves as fast endpoints. The following examples assume that you deploy these endpoints using FrankenPHP.
1. Serving the Quiz XML
Create a file called getQuiz.php that reads the XML file and returns it:
<?php // backend/api/getQuiz.php header('Content-Type: text/xml'); $xmlFile = __DIR__ . '/../quiz.xml'; if (file_exists($xmlFile)) { readfile($xmlFile); } else { http_response_code(404); echo '<error>Quiz not found</error>'; }
Place your XML file (formatted as in your sample) in the backend folder (for example, at
backend/quiz.xml
).
2. Submitting Quiz Results
Create a file called submit_result.php to accept incoming POST requests with JSON data and store results in the MySQL database:
<?php // backend/api/submit_result.php header('Content-Type: application/json'); // Include database configuration (adjust credentials as needed) require_once __DIR__ . '/../db_config.php'; try { // Use PDO for database interactions $dsn = "mysql:host={$db_host};dbname={$db_name};charset=utf8mb4"; $pdo = new PDO($dsn, $db_user, $db_pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); } catch (PDOException $e) { http_response_code(500); echo json_encode(['error' => 'Database connection failed.']); exit; } // Get the JSON POST data $data = json_decode(file_get_contents('php://input'), true); if (!$data) { http_response_code(400); echo json_encode(['error' => 'Invalid input']); exit; } $username = isset($data['username']) ? $data['username'] : 'anonymous'; $score = isset($data['score']) ? (int)$data['score'] : 0; $stmt = $pdo->prepare("INSERT INTO results (username, score, created_at) VALUES (?, ?, NOW())"); $stmt->execute([$username, $score]); echo json_encode([ 'success' => true, 'resultId' => $pdo->lastInsertId() ]);
3. Database Configuration
Create a file db_config.php with your MySQL connection details:
<?php // backend/db_config.php $db_host = 'localhost'; $db_name = 'quizdb'; $db_user = 'quizuser'; $db_pass = 'quizpass';
4. MySQL Table Creation
Run the following SQL query to create the table to store quiz results:
CREATE TABLE results ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) NOT NULL, score INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
Integrating with FrankenPHP
FrankenPHP is designed to let you run PHP code quickly on a modern webserver with routing similar to front-end frameworks. With the endpoints above (e.g.
/api/getQuiz.php
and/api/submit_result.php
), configure your FrankenPHP routing so that requests are directed to these files. (Check the FrankenPHP documentation for details on setting up routing and configuration.)
Next Steps and Enhancements
- XML Parsing Robustness: Depending on your complete XML structure, you may need to adjust the parsing logic. Consider leveraging a dedicated library (or server‐side processing in PHP) to convert XML to JSON.
- State Management: For a richer quiz experience, add timers, per-question feedback, and a review screen for missed questions.
- Styling and UX: Use CSS (or a UI library like Material UI) to enhance the visuals and interactivity of your quiz.
- Security: Ensure the PHP endpoints validate and sanitize inputs appropriately.
- Extended Data: You might want to also load additional metadata (like quiz title, instructions, etc.) from the XML and render that in your app.
This example should give you a sound starting point for building an interactive quiz application that marries a React frontend with a PHP/MySQL backend powered by FrankenPHP. Happy coding!
-
Elmer Masters
Can I activate the block editor with this old theme?
-
Elmer Masters
Current website traffic is all about awards.