Recent Updates Toggle Comment Threads | Keyboard Shortcuts

  • Elmer Masters 11:15 am on June 19, 2025 Permalink
    Tags: ,   

    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.

     
  • Elmer Masters 5:18 pm on May 30, 2025 Permalink
    Tags: CALIcon25,   

    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 4:24 pm on May 30, 2025 Permalink
    Tags:   

    Onto PR#53. More Awards and an update to Drupal core.

     
  • Elmer Masters 4:19 pm on May 30, 2025 Permalink
    Tags:   

    PR#52 merged into develop. Really need to fix datetime handling for LessonRun views

     
  • Elmer Masters 10:54 am on May 30, 2025 Permalink
    Tags:   

    Resolving merge conflicts in PR#52

     
  • Elmer Masters 5:17 pm on May 29, 2025 Permalink
    Tags: CALI Awards,   

    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 2:54 pm on May 29, 2025 Permalink
    Tags:   

    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 11:36 am on May 23, 2025 Permalink
    Tags: FrankenPHP, , React   

    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 11:32 am on May 23, 2025 Permalink  

    Can I activate the block editor with this old theme?

     
  • Elmer Masters 10:34 am on May 23, 2025 Permalink
    Tags: Awards,   

    Current website traffic is all about awards.

     
c
Compose new post
j
Next post/Next comment
k
Previous post/Previous comment
r
Reply
e
Edit
o
Show/Hide comments
t
Go to top
l
Go to login
h
Show/Hide help
shift + esc
Cancel
Secret Link