Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions components/FeaturesSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import React, { useState } from 'react';
import useIsMobile from '../hooks/useIsMobile';
import {
Monitor,
Bell,
Expand All @@ -10,7 +11,13 @@ import {
} from 'lucide-react';
import FeatureCard from './FeatureCard';

const MOBILE_VISIBLE_COUNT = 4;

const FeaturesSection: React.FC = () => {
const isMobile = useIsMobile();

const [isExpanded, setIsExpanded] = useState(false);

const features = [
{
icon: Monitor,
Expand Down Expand Up @@ -54,10 +61,20 @@ const FeaturesSection: React.FC = () => {
}
];

const featuresToDisplay =
(isMobile && !isExpanded)
? features.slice(0, MOBILE_VISIBLE_COUNT)
: features;

const handleToggle = () => {
setIsExpanded(!isExpanded);
};

const requiresToggle = isMobile && (features.length > MOBILE_VISIBLE_COUNT);

return (
<section id="features" className="py-16 features-section">
<div className="container-max">
{/* Section Header */}
<div className="text-center mb-12">
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-surface-100 mb-3">
Everything You Need to{' '}
Expand All @@ -69,9 +86,8 @@ const FeaturesSection: React.FC = () => {
</p>
</div>

{/* Features Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{features.map((feature, index) => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
{featuresToDisplay.map((feature, index) => (
<FeatureCard
key={index}
icon={feature.icon}
Expand All @@ -81,10 +97,20 @@ const FeaturesSection: React.FC = () => {
))}
</div>

{requiresToggle && (
<div className="flex justify-center mt-8 md:hidden">
<button
onClick={handleToggle}
className="py-2 px-6 text-sm font-semibold rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors duration-300"
>
{isExpanded ? 'Show Less Features' : 'Show More Features'}
</button>
</div>
)}
Comment on lines +100 to +109
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add accessibility attributes to the toggle button.

The toggle button is missing ARIA attributes that are essential for screen reader users to understand the button's purpose and state.

Apply this diff to add proper accessibility:

        {requiresToggle && (
            <div className="flex justify-center mt-8 md:hidden"> 
                <button
                    onClick={handleToggle}
+                   aria-expanded={isExpanded}
+                   aria-controls="features-grid"
+                   aria-label={isExpanded ? 'Show fewer features' : 'Show more features'}
                    className="py-2 px-6 text-sm font-semibold rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors duration-300"
                >
                    {isExpanded ? 'Show Less Features' : 'Show More Features'}
                </button>
            </div>
        )}

Also add the corresponding id to the features grid:

-        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
+        <div id="features-grid" className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{requiresToggle && (
<div className="flex justify-center mt-8 md:hidden">
<button
onClick={handleToggle}
className="py-2 px-6 text-sm font-semibold rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors duration-300"
>
{isExpanded ? 'Show Less Features' : 'Show More Features'}
</button>
</div>
)}
{requiresToggle && (
<div className="flex justify-center mt-8 md:hidden">
<button
onClick={handleToggle}
aria-expanded={isExpanded}
aria-controls="features-grid"
aria-label={isExpanded ? 'Show fewer features' : 'Show more features'}
className="py-2 px-6 text-sm font-semibold rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors duration-300"
>
{isExpanded ? 'Show Less Features' : 'Show More Features'}
</button>
</div>
)}
🤖 Prompt for AI Agents
In components/FeaturesSection.tsx around lines 100 to 109, the toggle button
lacks ARIA attributes and the features grid has no id; add accessibility by
giving the features grid a stable id (e.g., id="features-grid") and update the
button to include aria-expanded={isExpanded} and aria-controls="features-grid"
plus a descriptive aria-label (e.g., aria-label={isExpanded ? 'Collapse
features' : 'Expand features'}) so screen readers know the button's purpose and
state.


</div>
</section>
);
};

export default FeaturesSection;
export default FeaturesSection;
22 changes: 22 additions & 0 deletions hooks/useIsMobile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useState, useEffect } from 'react';

const useIsMobile = (breakpoint = 768) => {
const [isMobile, setIsMobile] = useState(
typeof window !== 'undefined' ? window.innerWidth < breakpoint : false
);

useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < breakpoint);
};

window.addEventListener('resize', checkMobile);
checkMobile();

return () => window.removeEventListener('resize', checkMobile);
}, [breakpoint]);
Comment on lines +8 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add debounce/throttle to the resize event listener.

The resize event can fire 100+ times per second during active window resizing, causing excessive state updates and re-renders. This degrades performance, especially on lower-end mobile devices.

Apply this diff to add a debounced resize handler:

  useEffect(() => {
+   let timeoutId: NodeJS.Timeout;
+   
    const checkMobile = () => {
-     setIsMobile(window.innerWidth < breakpoint);
+     clearTimeout(timeoutId);
+     timeoutId = setTimeout(() => {
+       setIsMobile(window.innerWidth < breakpoint);
+     }, 150);
    };

    window.addEventListener('resize', checkMobile);
-   checkMobile();
+   // Initial check without debounce
+   setIsMobile(window.innerWidth < breakpoint);

-   return () => window.removeEventListener('resize', checkMobile);
+   return () => {
+     window.removeEventListener('resize', checkMobile);
+     clearTimeout(timeoutId);
+   };
  }, [breakpoint]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < breakpoint);
};
window.addEventListener('resize', checkMobile);
checkMobile();
return () => window.removeEventListener('resize', checkMobile);
}, [breakpoint]);
useEffect(() => {
let timeoutId: NodeJS.Timeout;
const checkMobile = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
setIsMobile(window.innerWidth < breakpoint);
}, 150);
};
window.addEventListener('resize', checkMobile);
// Initial check without debounce
setIsMobile(window.innerWidth < breakpoint);
return () => {
window.removeEventListener('resize', checkMobile);
clearTimeout(timeoutId);
};
}, [breakpoint]);
🤖 Prompt for AI Agents
In hooks/useIsMobile.ts around lines 8 to 17, the resize handler calls
setIsMobile on every resize event which can fire many times; debounce (or
throttle) the checkMobile function to limit updates. Create a debounced version
of checkMobile (using a small utility debounce or lodash.debounce) with a
sensible delay (e.g., 100–200ms), attach the debounced handler to the resize
event, call the debounced handler once immediately (or call the original
checkMobile once for initial state), and ensure the cleanup removes the
debounced listener and cancels any pending debounce timer on unmount to prevent
memory leaks.


return isMobile;
};

export default useIsMobile;