Skip to content

Conversation

@tubajavedd
Copy link

@tubajavedd tubajavedd commented Feb 10, 2026

This PR introduces skeleton loading placeholders for the Projects section on the Home page to improve perceived performance and user experience.

✨ What’s improved

Displays skeleton cards while project data is loading

Prevents blank spaces and layout shifts

Provides immediate visual feedback on slower networks

Improves accessibility using ARIA loading attributes

🛠️ Implementation details

Added a reusable SkeletonCard component

Implemented a loading state in pages/index.jsx

Skeletons match the final card layout to avoid reflow

Loading state is temporary and can be replaced with real async data later

♿ Accessibility

Uses aria-busy and aria-live="polite" to announce loading state to assistive technologies

🧪 Testing

Tested locally on the Home page

Verified skeletons appear on initial load and transition smoothly to project cards

Summary by CodeRabbit

  • New Features

    • Added skeleton loading states to display while content loads, improving perceived responsiveness.
    • Enhanced card interactions with smooth hover animations and upward translation effects.
  • Improvements

    • Updated card styling with rounded corners, borders, and improved visual hierarchy.
    • Added smooth transitions and chevron icon animations for better visual feedback.
    • Enhanced dark mode support with improved shadow effects.
    • Improved accessibility with screen reader enhancements.

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

The changes introduce a client-side loading state with skeleton card UI components displayed during data initialization. Styling enhancements add transitions, hover effects, and rounded borders across card components. A new SkeletonCard component and accompanying CSS animation support the loading experience with an 800ms delay before content renders.

Changes

Cohort / File(s) Summary
Loading State Implementation
src/components/skeletonCard.jsx, src/pages/index.jsx
New SkeletonCard component added. Home page now implements loading state with 800ms delay, shuffles 3 random projects on mount via useEffect, conditionally renders skeleton cards while loading or actual CardEffect components when ready. Added aria-busy and aria-live attributes for accessibility.
Styling & Transitions
src/components/Card.jsx, src/pages/projects.jsx, src/style/tailwind.css
Card component enhanced with rounded-2xl, border, background color, padding, and hover/dark-mode shadow transitions. Card.Cta and ChevronRightIcon updated with color transitions and group-hover transforms. Projects page card styling includes transition, cursor pointer, and upward-translate hover effect. New skeleton-card CSS utility class with pulse keyframes animation added.
Formatting
src/components/Banner.jsx
Whitespace and formatting adjustments around Apply button block; no functional changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant Home
    participant useEffect1
    participant useEffect2
    participant SkeletonCard
    participant CardEffect

    User->>Home: Visit home page
    Home->>useEffect1: Component mounts
    useEffect1->>Home: Set loading = true<br/>Shuffle & set randomProjects
    Home->>SkeletonCard: Render 3 SkeletonCards<br/>(loading = true)
    SkeletonCard-->>User: Display skeleton cards
    
    useEffect2->>useEffect2: Wait 800ms (timer)
    useEffect2->>Home: Set loading = false
    Home->>CardEffect: Render CardEffect components<br/>(for each project)
    CardEffect-->>User: Display actual cards with content
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A skeleton dances, then cards come alive,
With pulses and transitions, the page springs to thrive,
Eight hundred milliseconds of anticipation's sweet wait,
Then rounded corners and shadows align with their fate,
Loading states whisper, "I'm busy, just see,"
Till hover effects bloom—smooth as can be! 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'UI skeleton 569' is vague and generic, using a ticket reference number without describing the actual change. It does not convey meaningful information about adding skeleton loading placeholders to the home page. Consider using a more descriptive title like 'Add skeleton loading state to projects section on home page' that clearly summarizes the main change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/index.jsx (1)

24-39: ⚠️ Potential issue | 🔴 Critical

Critical: Duplicate useEffect — Lines 37-39 mutate the source array and overwrite the correct shuffle.

There are two useEffect(() => { ... }, []) hooks that both shuffle and set randomProjects on mount. The second one (Lines 37-39) is leftover code that:

  1. Mutates the imported projects array in-place via .sort() (no spread/copy), corrupting the shared module-level array for any other consumer.
  2. Overwrites the result of the first effect (Lines 24-27), which correctly uses [...projects] to avoid mutation.

Remove the duplicate effect entirely.

🐛 Fix — remove the duplicate effect
   useEffect(() => {
     const shuffled = [...projects].sort(() => 0.5 - Math.random()).slice(0, 3)
     setRandomProjects(shuffled)
   }, [])
 
   useEffect(() => {
     const timer = setTimeout(() => {
       setLoading(false)
     }, 800)
 
     return () => clearTimeout(timer)
   }, [])
 
-  useEffect(() => {
-    setRandomProjects(projects.sort(() => 0.5 - Math.random()).slice(0, 3))
-  }, [])
🤖 Fix all issues with AI agents
In `@src/components/skeletonCard.jsx`:
- Line 3: Remove the redundant aria-busy from the skeleton card and add an
accessible, visually-hidden status message so screen readers hear that content
is loading: in src/components/skeletonCard.jsx update the <div
className="skeleton-card" role="status"> to include a hidden text node (e.g., a
span with a "sr-only" or visually-hidden class) such as "Loading content" and
remove the aria-busy attribute (the parent grid in index.jsx already provides
aria-busy="true"), ensuring the role="status" has accessible text for assistive
technologies.

In `@src/pages/index.jsx`:
- Around line 29-35: The artificial 800ms delay in the useEffect (the
setTimeout/clearTimeout timer that calls setLoading(false)) creates unnecessary
latency even though the statically imported projects data is immediately
available; remove the timeout and instead derive loading from actual data
readiness (e.g., initialize loading to false or
setLoading(Boolean(!projects.length)) and remove the timer code in the
useEffect), or if you only want a transient skeleton for the first render, drop
the setTimeout and let the component render once with loading true then
immediately set loading false synchronously based on the presence of the
imported projects; update or remove the useEffect, timer, and clearTimeout
references accordingly (functions/identifiers: useEffect, setLoading, timer,
clearTimeout, projects).

In `@src/pages/projects.jsx`:
- Around line 35-53: The card (MuiCard) sets cursor: 'pointer' but only the
inner <a> is clickable, causing a misleading affordance; either remove the
cursor style from the MuiCard sx prop or make the entire card clickable by
moving the link/onclick to the MuiCard (e.g., wrap MuiCard with the anchor/Link
or add an onClick that navigates to the same href and set role="link" and
tabIndex for accessibility); update the MuiCard's sx to drop cursor: 'pointer'
if you choose not to make the whole card act as a link.
- Around line 48-51: The boxShadow style inside the hover style object in
src/pages/projects.jsx uses an invalid alpha value (rgba(255,255,0,255)); update
the boxShadow value to use a valid alpha between 0 and 1 (for example
'rgba(255,255,0,1)') or an equivalent color string; locate the '&:hover' style
block that contains transform and boxShadow and replace the alpha 255 with 1 to
fix the CSS.
🧹 Nitpick comments (6)
src/style/tailwind.css (2)

4-10: Hardcoded dark background won't adapt to light mode.

#2a2a2a looks correct on a dark theme but will appear as a dark rectangle on a light-themed page, creating a jarring visual mismatch. Consider adding a light-mode variant (e.g., #e5e7eb) or converting this to Tailwind utility classes with dark: variants so it respects the theme.

Also, Tailwind already ships an animate-pulse utility. Rolling a custom keyframe duplicates built-in functionality.

♻️ Suggested approach using Tailwind utilities instead of custom CSS

Replace the custom CSS class with Tailwind utilities directly on the SkeletonCard div:

- <div className="skeleton-card" aria-busy="true" role="status" />
+ <div
+   className="h-[180px] w-full rounded-lg bg-zinc-200 dark:bg-zinc-800 animate-pulse"
+   aria-busy="true"
+   role="status"
+ />

This eliminates the need for the custom .skeleton-card class and @keyframes pulse entirely, and automatically handles light/dark themes.


9-16: Missing prefers-reduced-motion consideration.

Users who prefer reduced motion will still see the continuous pulse animation. Consider wrapping the animation in a @media (prefers-reduced-motion: no-preference) query, or using Tailwind's motion-safe:animate-pulse if you switch to utility classes.

src/components/Banner.jsx (1)

19-27: Inconsistent indentation in the reformatted block.

The <Link> and <a> elements lost their nesting indentation relative to the parent <div>. This is a formatting-only change but the result is harder to scan than the surrounding JSX.

src/components/Card.jsx (1)

59-71: Inconsistent indentation inside Card.Cta.

The <ChevronRightIcon> on Line 67 and the closing </Link> on Line 69 are not aligned with the rest of the JSX tree. Minor formatting nit.

src/components/skeletonCard.jsx (1)

1-5: File naming inconsistency — use PascalCase to match other components.

Other component files in src/components/ use PascalCase (Card.jsx, Banner.jsx, CardEffect.jsx). This file should be SkeletonCard.jsx to stay consistent.

src/pages/index.jsx (1)

22-35: Inconsistent indentation breaks readability.

Lines 23-35 are flush-left or under-indented compared to the component body (Line 22 is indented correctly at 2 spaces inside the function). This makes the hook boundaries hard to spot visually.

@@ -0,0 +1,5 @@
export default function SkeletonCard() {
return (
<div className="skeleton-card" aria-busy="true" role="status" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

role="status" without accessible text is silent to screen readers.

The div has no text content, so assistive technologies will announce nothing for this live region. Add a visually-hidden label so users know content is loading.

Also, aria-busy="true" is already set on the parent grid container in index.jsx, making it redundant here on each individual card.

♻️ Suggested fix
- <div className="skeleton-card" aria-busy="true" role="status" />
+ <div className="skeleton-card" role="status">
+   <span className="sr-only">Loading project…</span>
+ </div>
📝 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
<div className="skeleton-card" aria-busy="true" role="status" />
<div className="skeleton-card" role="status">
<span className="sr-only">Loading project…</span>
</div>
🤖 Prompt for AI Agents
In `@src/components/skeletonCard.jsx` at line 3, Remove the redundant aria-busy
from the skeleton card and add an accessible, visually-hidden status message so
screen readers hear that content is loading: in src/components/skeletonCard.jsx
update the <div className="skeleton-card" role="status"> to include a hidden
text node (e.g., a span with a "sr-only" or visually-hidden class) such as
"Loading content" and remove the aria-busy attribute (the parent grid in
index.jsx already provides aria-busy="true"), ensuring the role="status" has
accessible text for assistive technologies.

Comment on lines +29 to +35
useEffect(() => {
const timer = setTimeout(() => {
setLoading(false)
}, 800)

return () => clearTimeout(timer)
}, [])
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

Artificial 800ms delay when data is already synchronously available.

The projects data is statically imported (Line 17) — it's available immediately. The 800ms setTimeout creates a fake loading state that adds latency on every page visit for no functional benefit. The PR notes this is "temporary and replaceable by real async data later," but until then it actively degrades perceived performance — the opposite of the stated goal.

Consider tying the loading state to actual data readiness instead:

♻️ Suggested simplification
- const [randomProjects, setRandomProjects] = useState([])
- const [loading, setLoading] = useState(true)
- useEffect(() => {
-   const shuffled = [...projects].sort(() => 0.5 - Math.random()).slice(0, 3)
-   setRandomProjects(shuffled)
- }, [])
-
- useEffect(() => {
-   const timer = setTimeout(() => {
-     setLoading(false)
-   }, 800)
-
-   return () => clearTimeout(timer)
- }, [])
+ const [randomProjects, setRandomProjects] = useState([])
+ const loading = randomProjects.length === 0
+
+ useEffect(() => {
+   const shuffled = [...projects].sort(() => 0.5 - Math.random()).slice(0, 3)
+   setRandomProjects(shuffled)
+ }, [])

This shows skeletons only for the single render cycle before the effect fires, matching the actual data lifecycle.

🤖 Prompt for AI Agents
In `@src/pages/index.jsx` around lines 29 - 35, The artificial 800ms delay in the
useEffect (the setTimeout/clearTimeout timer that calls setLoading(false))
creates unnecessary latency even though the statically imported projects data is
immediately available; remove the timeout and instead derive loading from actual
data readiness (e.g., initialize loading to false or
setLoading(Boolean(!projects.length)) and remove the timer code in the
useEffect), or if you only want a transient skeleton for the first render, drop
the setTimeout and let the component render once with loading true then
immediately set loading false synchronously based on the presence of the
imported projects; update or remove the useEffect, timer, and clearTimeout
references accordingly (functions/identifiers: useEffect, setLoading, timer,
clearTimeout, projects).

Comment on lines +35 to +53
<MuiCard
className='dark:bg-[#2A2A2A] dark:border-white'
sx={{
height: 400,
borderRadius: 2,
border: '1px solid',
borderColor: '#3c982c',
boxShadow: '0px 4px 4px #00000040',
backdropFilter: 'blur(4px) brightness(100%)',
display: 'flex',
flexDirection: 'column',
transition: 'all 0.3s ease',
cursor: 'pointer',
'&:hover': {
transform: 'translateY(-6px)',
boxShadow: '1px 2px 1px 1px rgba(255,255,0,255)',
},
}}
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

cursor: 'pointer' on the card implies the entire card is clickable, but only the inner link is.

The pointer cursor appears over the entire card surface, yet clicking anywhere outside the <a> tag does nothing. This creates a misleading affordance. Either make the whole card a clickable link or remove the cursor: 'pointer'.

🤖 Prompt for AI Agents
In `@src/pages/projects.jsx` around lines 35 - 53, The card (MuiCard) sets cursor:
'pointer' but only the inner <a> is clickable, causing a misleading affordance;
either remove the cursor style from the MuiCard sx prop or make the entire card
clickable by moving the link/onclick to the MuiCard (e.g., wrap MuiCard with the
anchor/Link or add an onClick that navigates to the same href and set
role="link" and tabIndex for accessibility); update the MuiCard's sx to drop
cursor: 'pointer' if you choose not to make the whole card act as a link.

Comment on lines +48 to +51
'&:hover': {
transform: 'translateY(-6px)',
boxShadow: '1px 2px 1px 1px rgba(255,255,0,255)',
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Invalid rgba alpha value — use 1 instead of 255.

rgba(255,255,0,255) uses 255 for the alpha channel, but CSS rgba() expects a value between 0 and 1 (or 0%100%). Browsers silently clamp this to 1, so it works today, but it's technically invalid and confusing to future readers.

🐛 Fix
-      boxShadow: '1px 2px 1px 1px rgba(255,255,0,255)',
+      boxShadow: '1px 2px 1px 1px rgba(255, 255, 0, 1)',
📝 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
'&:hover': {
transform: 'translateY(-6px)',
boxShadow: '1px 2px 1px 1px rgba(255,255,0,255)',
},
'&:hover': {
transform: 'translateY(-6px)',
boxShadow: '1px 2px 1px 1px rgba(255, 255, 0, 1)',
},
🤖 Prompt for AI Agents
In `@src/pages/projects.jsx` around lines 48 - 51, The boxShadow style inside the
hover style object in src/pages/projects.jsx uses an invalid alpha value
(rgba(255,255,0,255)); update the boxShadow value to use a valid alpha between 0
and 1 (for example 'rgba(255,255,0,1)') or an equivalent color string; locate
the '&:hover' style block that contains transform and boxShadow and replace the
alpha 255 with 1 to fix the CSS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant