Los Angeles

QR Code Matrix Component

A technical deep dive into building an interactive QR code matrix component with database integration.

Component Architecture

This component was built to visualize the progressive phases of QR code encoding on a 25x25 matrix. It fetches encoding data from a Neon PostgreSQL database using Drizzle ORM and renders an interactive grid that users can navigate through.

View the complete project overview →

Component Demo

Below is a live demonstration of the GoQRMatrix component. Navigate through the phases to see how the QR code is built up layer by layer.

Loading...

Component Implementation

Database Schema

The QR encoding phases are stored in a PostgreSQL database using the following schema:

schema.tsx
import { pgTable, serial, text, json, varchar } from 'drizzle-orm/pg-core';

export const qrEncodingPhases = pgTable('qr_encoding_phases', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 100 }).notNull(),
  description: text('description').notNull(),
  matrixData: json('matrix_data').notNull(), // 25x25 matrix representation
  order: serial('order').notNull(), // For ordering the phases
});

export type QREncodingPhase = {
  id: number;
  name: string;
  description: string;
  matrixData: boolean[][]; // 25x25 matrix where true = filled cell, false = empty
  order: number;
};

GoQRMatrix Component

The core component that renders the interactive QR code matrix:

GoQRMatrix.tsx
"use client";
import React, { useState, useEffect } from 'react';
import { QREncodingPhase } from '../db/schema';

interface GoQRMatrixProps {
  phases: QREncodingPhase[];
  initialPhase?: number;
}

export default function GoQRMatrix({ phases, initialPhase = 0 }: GoQRMatrixProps) {
  const [currentPhaseIndex, setCurrentPhaseIndex] = useState(initialPhase);
  const [currentPhase, setCurrentPhase] = useState<QREncodingPhase | null>(null);

  useEffect(() => {
    if (phases && phases.length > 0) {
      setCurrentPhase(phases[currentPhaseIndex]);
    }
  }, [phases, currentPhaseIndex]);

  const goToNextPhase = () => {
    if (currentPhaseIndex < phases.length - 1) {
      setCurrentPhaseIndex(currentPhaseIndex + 1);
    }
  };

  const goToPreviousPhase = () => {
    if (currentPhaseIndex > 0) {
      setCurrentPhaseIndex(currentPhaseIndex - 1);
    }
  };

  if (!currentPhase) return <div>Loading...</div>;

  return (
    <div className="flex flex-col items-center">
      <div className="mb-6">
        <h3 className="text-2xl font-bold mb-2">{currentPhase.name}</h3>
        <p className="text-gray-300">{currentPhase.description}</p>
      </div>
      
      {/* QR Matrix Grid */}
      <div className="bg-amber-100 p-4 rounded-lg shadow-lg">
        <div 
          className="grid gap-0.5"
          style={{ 
            gridTemplateColumns: 'repeat(25, minmax(0, 1fr))',
            width: '500px',
            height: '500px'
          }}
        >
          {currentPhase.matrixData.flat().map((cell, index) => (
            <div 
              key={index}
              className={`aspect-square ${cell ? 'bg-black' : 'bg-amber-100'} border border-amber-200`}
            />
          ))}
        </div>
      </div>
      
      {/* Navigation Controls */}
      <div className="flex items-center justify-between w-full max-w-md mt-8">
        <button 
          onClick={goToPreviousPhase}
          disabled={currentPhaseIndex === 0}
          className={`px-4 py-2 rounded-lg ${
            currentPhaseIndex === 0 
              ? 'bg-gray-600 cursor-not-allowed' 
              : 'bg-blue-600 hover:bg-blue-700'
          } transition`}
        >
          Previous Phase
        </button>
        
        <div className="text-center">
          <span className="text-sm text-gray-400">
            Phase {currentPhaseIndex + 1} of {phases.length}
          </span>
        </div>
        
        <button 
          onClick={goToNextPhase}
          disabled={currentPhaseIndex === phases.length - 1}
          className={`px-4 py-2 rounded-lg ${
            currentPhaseIndex === phases.length - 1 
              ? 'bg-gray-600 cursor-not-allowed' 
              : 'bg-blue-600 hover:bg-blue-700'
          } transition`}
        >
          Next Phase
        </button>
      </div>
    </div>
  );
}

Database Integration

Here's how the component fetches data from the Neon PostgreSQL database using Drizzle ORM:

qrData.ts
import { db } from '../db/client';
import { qrEncodingPhases } from '../db/schema';
import { eq } from 'drizzle-orm';

// Function to fetch all QR encoding phases
export async function getQREncodingPhases() {
  try {
    const phases = await db.query.qrEncodingPhases.findMany({
      orderBy: (phases) => phases.order,
    });
    return phases;
  } catch (error) {
    console.error('Error fetching QR encoding phases:', error);
    throw error;
  }
}

// Function to fetch a specific phase by ID
export async function getQREncodingPhaseById(id: number) {
  try {
    const phase = await db.query.qrEncodingPhases.findFirst({
      where: eq(qrEncodingPhases.id, id),
    });
    return phase;
  } catch (error) {
    console.error('Error fetching QR encoding phase:', error);
    throw error;
  }
}

Implementation Challenges

Matrix Representation

Storing and manipulating a 25x25 boolean matrix required careful consideration of data structures and serialization methods. JSON was chosen for its flexibility and ease of use with PostgreSQL.

Performance Optimization

Rendering 625 grid cells efficiently required performance optimizations, including memoization and careful state management to prevent unnecessary re-renders.

Responsive Design

Creating a responsive grid that maintains its aspect ratio across different screen sizes was achieved using CSS Grid and custom styling.

Database Integration

Integrating with Neon PostgreSQL required setting up proper connection pooling and error handling to ensure reliable data fetching.

Future Enhancements

The component could be extended with the following features:

  • Animation transitions between phases to visualize the changes more clearly
  • Interactive mode allowing users to place stones manually and compare with the correct pattern
  • Custom data encoding to generate QR codes for user-provided URLs or text
  • 3D visualization mode to better represent the physical Go board

Want to use this component?

The GoQRMatrix component is available as part of our open-source library. Check out the documentation and source code.