localgreenchain/components/transport/QRCodeDisplay.tsx
Claude 0cce5e2345
Add UI components for transport tracking, demand visualization, and analytics
Transport components:
- TransportTimeline: Visual timeline of transport events with status badges
- JourneyMap: SVG-based map visualization of plant journey locations
- CarbonFootprintCard: Carbon metrics display with comparison charts
- QRCodeDisplay: QR code generation for traceability verification
- TransportEventForm: Form for recording transport events

Demand components:
- DemandSignalCard: Regional demand signal with supply status indicators
- PreferencesForm: Multi-section consumer preference input form
- RecommendationList: Planting recommendations with risk assessment
- SupplyGapChart: Supply vs demand visualization with gap indicators
- SeasonalCalendar: Seasonal produce availability calendar view

Analytics components:
- EnvironmentalImpact: Comprehensive carbon and food miles analysis
- FoodMilesTracker: Food miles tracking with daily charts and targets
- SavingsCalculator: Environmental savings vs conventional agriculture

All components follow existing patterns, use Tailwind CSS, and are fully typed.
2025-11-22 18:34:51 +00:00

217 lines
7.5 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { TransportQRData } from '../../lib/transport/types';
interface QRCodeDisplayProps {
qrData: TransportQRData;
size?: number;
showDetails?: boolean;
}
// Simple QR code matrix generator (basic implementation)
function generateQRMatrix(data: string, size: number = 21): boolean[][] {
// This is a simplified representation - in production you'd use a library like 'qrcode'
const matrix: boolean[][] = Array(size)
.fill(null)
.map(() => Array(size).fill(false));
// Add finder patterns (corners)
const addFinderPattern = (row: number, col: number) => {
for (let r = 0; r < 7; r++) {
for (let c = 0; c < 7; c++) {
if (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4)) {
if (row + r < size && col + c < size) {
matrix[row + r][col + c] = true;
}
}
}
}
};
addFinderPattern(0, 0);
addFinderPattern(0, size - 7);
addFinderPattern(size - 7, 0);
// Add some data-based pattern
const hash = data.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
for (let i = 8; i < size - 8; i++) {
for (let j = 8; j < size - 8; j++) {
matrix[i][j] = ((i * j + hash) % 3) === 0;
}
}
return matrix;
}
export default function QRCodeDisplay({ qrData, size = 200, showDetails = true }: QRCodeDisplayProps) {
const [copied, setCopied] = useState(false);
const [downloading, setDownloading] = useState(false);
const qrString = JSON.stringify(qrData);
const matrix = generateQRMatrix(qrString);
const cellSize = size / matrix.length;
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(qrData.quickLookupUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const handleDownload = () => {
setDownloading(true);
// Create SVG for download
const svgContent = `
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
<rect width="100%" height="100%" fill="white"/>
${matrix
.map((row, i) =>
row
.map((cell, j) =>
cell
? `<rect x="${j * cellSize}" y="${i * cellSize}" width="${cellSize}" height="${cellSize}" fill="black"/>`
: ''
)
.join('')
)
.join('')}
</svg>
`;
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `qr-${qrData.plantId || qrData.batchId || 'code'}.svg`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
setDownloading(false);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
return (
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white p-4">
<h3 className="text-lg font-bold">Traceability QR Code</h3>
<p className="text-purple-200 text-sm">Scan to verify authenticity</p>
</div>
<div className="p-6">
{/* QR Code */}
<div className="flex justify-center mb-6">
<div className="p-4 bg-white border-2 border-gray-200 rounded-lg shadow-inner">
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<rect width="100%" height="100%" fill="white" />
{matrix.map((row, i) =>
row.map((cell, j) =>
cell ? (
<rect
key={`${i}-${j}`}
x={j * cellSize}
y={i * cellSize}
width={cellSize}
height={cellSize}
fill="black"
/>
) : null
)
)}
</svg>
</div>
</div>
{/* Action buttons */}
<div className="flex gap-3 mb-6">
<button
onClick={handleCopyLink}
className="flex-1 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition flex items-center justify-center gap-2"
>
{copied ? (
<>
<span className="text-green-600"></span> Copied!
</>
) : (
<>
<span>📋</span> Copy Link
</>
)}
</button>
<button
onClick={handleDownload}
disabled={downloading}
className="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium transition flex items-center justify-center gap-2 disabled:opacity-50"
>
{downloading ? (
<>Downloading...</>
) : (
<>
<span></span> Download
</>
)}
</button>
</div>
{/* Details */}
{showDetails && (
<div className="space-y-3 border-t border-gray-200 pt-4">
{qrData.plantId && (
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">Plant ID</span>
<span className="text-sm font-mono text-gray-900">{qrData.plantId.slice(0, 12)}...</span>
</div>
)}
{qrData.batchId && (
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">Batch ID</span>
<span className="text-sm font-mono text-gray-900">{qrData.batchId.slice(0, 12)}...</span>
</div>
)}
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">Last Event</span>
<span className="text-sm text-gray-900 capitalize">
{qrData.lastEventType.replace(/_/g, ' ')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">Last Updated</span>
<span className="text-sm text-gray-900">{formatDate(qrData.lastEventTimestamp)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">Current Custodian</span>
<span className="text-sm text-gray-900">{qrData.currentCustodian}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">Verification Code</span>
<span className="text-sm font-mono text-green-600">{qrData.verificationCode}</span>
</div>
</div>
)}
{/* Blockchain info */}
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
<div className="flex items-center gap-2 text-sm text-gray-600">
<span>🔗</span>
<span className="font-medium">Blockchain Verified</span>
</div>
<p className="text-xs text-gray-400 mt-1 font-mono break-all">
{qrData.blockchainAddress}
</p>
</div>
</div>
</div>
);
}