feat(card): add visibility handling and loading placeholders for FormatCard and RegexCard components to improve performance

This commit is contained in:
Sam Chau
2025-08-27 03:23:24 +09:30
parent 77f996f8c5
commit 666f98c68b
2 changed files with 67 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
import React, {useState} from 'react'; import React, {useState, useEffect, useRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Copy, Check, FlaskConical, FileText, ListFilter} from 'lucide-react'; import {Copy, Check, FlaskConical, FileText, ListFilter} from 'lucide-react';
import Tooltip from '@ui/Tooltip'; import Tooltip from '@ui/Tooltip';
@@ -14,6 +14,8 @@ function FormatCard({
willBeSelected, willBeSelected,
onSelect onSelect
}) { }) {
const [isVisible, setIsVisible] = useState(false);
const cardRef = useRef(null);
const [showDescription, setShowDescription] = useState(() => { const [showDescription, setShowDescription] = useState(() => {
const saved = localStorage.getItem(`format-view-${format.file_name}`); const saved = localStorage.getItem(`format-view-${format.file_name}`);
return saved !== null ? JSON.parse(saved) : true; return saved !== null ? JSON.parse(saved) : true;
@@ -64,8 +66,27 @@ function FormatCard({
} }
}; };
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIsVisible(entry.isIntersecting);
},
{
threshold: 0,
rootMargin: '100px' // Keep cards rendered 100px outside viewport
}
);
if (cardRef.current) {
observer.observe(cardRef.current);
}
return () => observer.disconnect();
}, []);
return ( return (
<div <div
ref={cardRef}
className={`w-full h-[12rem] bg-gradient-to-br from-gray-800/95 to-gray-900 border ${ className={`w-full h-[12rem] bg-gradient-to-br from-gray-800/95 to-gray-900 border ${
isSelected isSelected
? 'border-blue-500' ? 'border-blue-500'
@@ -81,7 +102,8 @@ function FormatCard({
} transition-all cursor-pointer relative`} } transition-all cursor-pointer relative`}
onClick={handleClick} onClick={handleClick}
onMouseDown={handleMouseDown}> onMouseDown={handleMouseDown}>
<div className='p-4 flex flex-col h-full'> {isVisible ? (
<div className='p-4 flex flex-col h-full'>
{/* Header Section */} {/* Header Section */}
<div className='flex justify-between items-start'> <div className='flex justify-between items-start'>
<div className='flex flex-col min-w-0 flex-1'> <div className='flex flex-col min-w-0 flex-1'>
@@ -237,6 +259,15 @@ function FormatCard({
)} )}
</div> </div>
</div> </div>
) : (
<div className='p-4 flex items-center justify-center h-full'>
<div className='w-full space-y-2'>
<div className='h-5 bg-gray-700/50 rounded animate-pulse'/>
<div className='h-3 bg-gray-700/50 rounded animate-pulse w-3/4'/>
<div className='h-3 bg-gray-700/50 rounded animate-pulse w-1/2'/>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, {useState, useEffect, useRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Copy, Check, FlaskConical} from 'lucide-react'; import {Copy, Check, FlaskConical} from 'lucide-react';
import Tooltip from '@ui/Tooltip'; import Tooltip from '@ui/Tooltip';
@@ -15,6 +15,9 @@ const RegexCard = ({
willBeSelected, willBeSelected,
onSelect onSelect
}) => { }) => {
const [isVisible, setIsVisible] = useState(false);
const cardRef = useRef(null);
const totalTests = pattern.tests?.length || 0; const totalTests = pattern.tests?.length || 0;
const passedTests = pattern.tests?.filter(t => t.passes)?.length || 0; const passedTests = pattern.tests?.filter(t => t.passes)?.length || 0;
const passRate = const passRate =
@@ -46,8 +49,27 @@ const RegexCard = ({
return 'text-red-400'; return 'text-red-400';
}; };
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIsVisible(entry.isIntersecting);
},
{
threshold: 0,
rootMargin: '100px' // Keep cards rendered 100px outside viewport
}
);
if (cardRef.current) {
observer.observe(cardRef.current);
}
return () => observer.disconnect();
}, []);
return ( return (
<div <div
ref={cardRef}
className={`w-full h-[20rem] bg-gradient-to-br from-gray-800/95 to-gray-900 border ${ className={`w-full h-[20rem] bg-gradient-to-br from-gray-800/95 to-gray-900 border ${
isSelected isSelected
? 'border-blue-500' ? 'border-blue-500'
@@ -63,7 +85,8 @@ const RegexCard = ({
} transition-all cursor-pointer overflow-hidden`} } transition-all cursor-pointer overflow-hidden`}
onClick={handleClick} onClick={handleClick}
onMouseDown={handleMouseDown}> onMouseDown={handleMouseDown}>
<div className='p-6 flex flex-col h-full'> {isVisible ? (
<div className='p-6 flex flex-col h-full'>
{/* Header Section */} {/* Header Section */}
<div className='flex-none'> <div className='flex-none'>
<div className='flex justify-between items-start'> <div className='flex justify-between items-start'>
@@ -183,6 +206,15 @@ const RegexCard = ({
)} )}
</div> </div>
</div> </div>
) : (
<div className='p-6 flex items-center justify-center h-full'>
<div className='w-full space-y-3'>
<div className='h-6 bg-gray-700/50 rounded animate-pulse'/>
<div className='h-20 bg-gray-700/50 rounded animate-pulse'/>
<div className='h-4 bg-gray-700/50 rounded animate-pulse w-3/4'/>
</div>
</div>
)}
</div> </div>
); );
}; };