import React, { useState } from 'react'; import { ChevronLeft, Braces, List, Type, Hash, ToggleLeft, Minus, Eye, Code, Copy, Check } from 'lucide-react'; const PostmanTable = ({ data, title = "JSON Data" }) => { const [currentPath, setCurrentPath] = useState([]); const [renderHtml, setRenderHtml] = useState(true); const [copiedItems, setCopiedItems] = useState(new Set()); // Get current data based on path const getCurrentData = () => { let current = data; for (const pathSegment of currentPath) { if (current && typeof current === 'object') { current = current[pathSegment]; } } return current; }; const currentData = getCurrentData(); // Check if current data is an array for horizontal table display const isArrayView = Array.isArray(currentData); const isObjectView = currentData && typeof currentData === 'object' && !Array.isArray(currentData); // Generate table headers for array view const getArrayHeaders = () => { if (!isArrayView || currentData.length === 0) return []; // Check if array contains objects or primitives const firstItem = currentData[0]; if (firstItem && typeof firstItem === 'object' && !Array.isArray(firstItem)) { // Array of objects - get all possible keys const headers = new Set(); currentData.forEach(item => { if (item && typeof item === 'object') { Object.keys(item).forEach(key => headers.add(key)); } }); return Array.from(headers); } else { // Array of primitives - use "Value" as header return ['Value']; } }; const headers = getArrayHeaders(); // Check if value contains HTML const isHtmlContent = (value) => { return value && typeof value === 'string' && (value.includes('<') && value.includes('>')) && /<[^>]+>/.test(value); }; // Copy value to clipboard const copyToClipboard = async (value, itemId) => { try { const textValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2); await navigator.clipboard.writeText(textValue); // Show feedback setCopiedItems(prev => new Set([...prev, itemId])); setTimeout(() => { setCopiedItems(prev => { const newSet = new Set(prev); newSet.delete(itemId); return newSet; }); }, 2500); } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = typeof value === 'string' ? value : JSON.stringify(value, null, 2); document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); // Show feedback for fallback too setCopiedItems(prev => new Set([...prev, itemId])); setTimeout(() => { setCopiedItems(prev => { const newSet = new Set(prev); newSet.delete(itemId); return newSet; }); }, 2500); } }; // Handle row click - navigate to item details const handleRowClick = (index, key = null) => { if (isArrayView) { setCurrentPath([...currentPath, index]); } else if (isObjectView && key) { // Navigate into object property setCurrentPath([...currentPath, key]); } }; // Handle back navigation const handleBack = () => { if (currentPath.length > 0) { const newPath = currentPath.slice(0, -1); setCurrentPath(newPath); } }; // Handle breadcrumb navigation const handleBreadcrumbClick = (index) => { if (index === 0) { // Click on "Root" - go to root setCurrentPath([]); } else { // Click on specific breadcrumb - navigate to that level const newPath = currentPath.slice(0, index); setCurrentPath(newPath); } }; // Generate breadcrumb const getBreadcrumb = () => { const parts = ['Root']; currentPath.forEach((segment, index) => { if (typeof segment === 'number') { parts.push(segment.toString()); } else { parts.push(segment); } }); return parts; }; // Format value for display in table (truncated) const formatValue = (value) => { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (typeof value === 'string') { // Truncate long strings for table display if (value.length > 100) { return value.substring(0, 100) + '...'; } // Replace newlines with spaces for single-line display return value.replace(/\n/g, ' ').replace(/\s+/g, ' '); } if (typeof value === 'boolean') return value.toString(); if (typeof value === 'number') return value.toString(); if (typeof value === 'object') { if (Array.isArray(value)) return `Array(${value.length})`; return `Object(${Object.keys(value).length})`; } return String(value); }; // Format value for display in details view (full text) const formatFullValue = (value) => { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (typeof value === 'string') return value; // Show full string without truncation if (typeof value === 'boolean') return value.toString(); if (typeof value === 'number') return value.toString(); if (typeof value === 'object') { if (Array.isArray(value)) return `Array(${value.length})`; return `Object(${Object.keys(value).length})`; } return String(value); }; // Get type-specific styling and icon const getTypeStyle = (value) => { if (value === null) { return { color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300', icon: , type: 'null' }; } if (Array.isArray(value)) { return { color: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300', icon: , type: 'array' }; } switch (typeof value) { case 'object': return { color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300', icon: , type: 'object' }; case 'string': return { color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300', icon: , type: 'string' }; case 'number': return { color: 'bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-300', icon: , type: 'number' }; case 'boolean': return { color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300', icon: , type: 'boolean' }; default: return { color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-300', icon: , type: 'unknown' }; } }; // Render value with appropriate styling (for table view) const renderValue = (value) => { const typeStyle = getTypeStyle(value); const formattedValue = formatValue(value); return ( {typeStyle.icon} {formattedValue} ); }; // Render value with full text (for detail view) const renderFullValue = (value) => { const typeStyle = getTypeStyle(value); const formattedValue = formatFullValue(value); const hasHtml = isHtmlContent(value); return (
{typeStyle.icon} {hasHtml && renderHtml ? (
) : ( formattedValue )} {/* HTML Toggle Buttons */} {hasHtml && (
)}
); }; // Get value type const getValueType = (value) => { if (value === null) return 'null'; if (Array.isArray(value)) return 'array'; return typeof value; }; return (
{/* Header with Breadcrumb */}
{currentPath.length > 0 && ( )}
{getBreadcrumb().map((part, index) => ( {index > 0 && /} ))}
{isArrayView && `${currentData.length} items`} {isObjectView && `${Object.keys(currentData).length} properties`}
{/* Content */}
{isArrayView ? ( // Horizontal table for arrays {headers.map(header => ( ))} {currentData.map((item, index) => { const isPrimitiveArray = headers.length === 1 && headers[0] === 'Value'; return ( handleRowClick(index)} className="hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer transition-colors duration-150" > {isPrimitiveArray ? ( ) : ( headers.map(header => ( )) )} ); })}
# {header}
{index} {renderValue(item)} {renderValue(item?.[header])}
) : isObjectView ? ( // Vertical key-value table for objects {Object.entries(currentData).map(([key, value]) => { const isClickable = typeof value === 'object' && value !== null; return ( isClickable && handleRowClick(null, key)} className={`transition-colors duration-150 ${ isClickable ? 'hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer' : 'hover:bg-gray-50 dark:hover:bg-gray-700' }`} > ); })}
Key Value
{key} {renderFullValue(value)}
) : ( // Fallback for primitive values
{formatFullValue(currentData)}
Type: {getValueType(currentData)}
)}
{/* Footer */}
{isArrayView ? `Array with ${currentData.length} items` : isObjectView ? `Object with ${Object.keys(currentData).length} properties` : `Primitive value (${getValueType(currentData)})`} Path: {currentPath.length === 0 ? 'Root' : currentPath.join(' → ')}
); }; export default PostmanTable;