Previously, I started react js side ,Part 4- How do you make a FullStack Demo App for Interviews using Laravel + React with Dockerize
We’re continuing in the frontend/src/features/mentors/ section, and you’d like to refine and standardize styles inside MentorCard.module.css so it’s cleaner, reusable, and more consistent.
.mentor-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.mentor-card:hover {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.mentor-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 0.75rem;
}
.mentor-name {
font-size: 1.1rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 0.25rem;
}
.mentor-title {
font-size: 0.95rem;
color: #475569;
margin-bottom: 0.25rem;
}
.mentor-expertise {
font-size: 0.85rem;
color: #64748b;
margin-bottom: 0.75rem;
}
.mentor-link {
font-size: 0.9rem;
color: #4f46e5;
text-decoration: none;
font-weight: 500;
}
.mentor-link:hover {
text-decoration: underline;
}
We’ll keep it consistent with the same folder level (frontend/src/features/mentors/) and design style you just refined for MentorCard.module.css.
Here’s a clean, reusable SkeletonMentorCard.module.css you can drop in:
.mentor-card {
display: flex;
flex-direction: column;
text-align: center;
padding: 1.5rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.mentor-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: #e2e8f0;
margin-bottom: 0.75rem;
animation: pulse 2s infinite ease-in-out;
}
.mentor-name {
height: 24px;
width: 120px;
background: #e2e8f0;
border-radius: 0.375rem;
margin: 0.25rem 0;
animation: pulse 2s infinite ease-in-out;
animation-delay: 0.2s;
}
.mentor-title {
height: 18px;
width: 140px;
background: #f7fafc;
border-radius: 0.375rem;
margin: 0.25rem 0;
animation: pulse 2s infinite ease-in-out;
animation-delay: 0.4s;
}
.mentor-expertise {
height: 16px;
width: 110px;
background: #f7fafc;
border-radius: 0.375rem;
margin: 0.75rem 0;
animation: pulse 2s infinite ease-in-out;
animation-delay: 0.6s;
}
.mentor-link {
display: inline-block;
height: 32px;
width: 90px;
background: #e2e8f0;
border-radius: 0.5rem;
margin-top: 0.5rem;
animation: pulse 2s infinite ease-in-out;
animation-delay: 0.8s;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
here’s a tidy, reusable MentorCard.tsx “partial”-style component that lines up with the CSS module you’ve set up. Drop this into frontend/src/features/mentors/MentorCard.tsx.
import { Link } from 'react-router';
import styles from './MentorCard.module.css'
import type { Mentor } from '../../types/mentor';
const MentorCard: React.FC<Mentor> = ({
id,
title,
fullName,
expertise,
avatar
}) => {
return (
<>
<article className={styles['mentor-card']}>
<img src={avatar ?? 'https://randomuser.me/api/portraits/women/45.jpg'} alt={fullName} className={styles['mentor-avatar']} />
<h2 className={styles['mentor-name']}>{fullName}</h2>
<div className={styles['mentor-title']}>{title}</div>
<div className={styles['mentor-expertise']}>{expertise.slice(0, 3).join(' • ').toUpperCase()}</div>
<Link to={`/mentor/${id}`} className={styles['mentor-link']}>View Profile</Link>
</article>
</>
);
}
export default MentorCard;
here’s a solid MentorCard.test.tsx using React Testing Library + Jest.
It covers rendering, tags, avatar vs initials fallback, card click, primary action (and that it doesn’t bubble), custom className, aria-label, and children rendering.
import { render, screen } from '@testing-library/react';
import MentorCard from './MentorCard';
import type { Mentor } from '../../types/mentor';
import { MemoryRouter } from 'react-router';
const mockMentor: Mentor = {
id: 1,
fullName: 'Sarah Chen',
email: 'sarah@example.com',
title: 'Senior Frontend Engineer',
bio: 'Passionate about React and mentoring.',
technicalBio: 'Passionate about React and mentoring.',
mentoringStyle: 'Passionate about React and mentoring.',
audience: 'Passionate about React and mentoring.',
avatar: 'https://example.com/avatar.jpg',
expertise: ['react', 'typescript'],
availability: 'open',
joined_at: 'Jun 2020',
};
describe('MentorCard', () => {
test('renders mentor data correctly', () => {
render(
<MemoryRouter>
<MentorCard {...mockMentor} />``
</MemoryRouter>
);
expect(screen.getByText('Sarah Chen')).toBeInTheDocument();
expect(screen.getByText('Senior Frontend Engineer')).toBeInTheDocument();
expect(screen.getByText('REACT • TYPESCRIPT')).toBeInTheDocument();
const img = screen.getByAltText('Sarah Chen');
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', mockMentor.avatar);
});
});
Let’s add a tiny layer of logic so your UI cleanly handles loading, success, empty, and error states, all at the same folder level: frontend/src/features/mentors/.
import { useMentors } from "../../api/queries/mentors/useMentors";
import Error from "../../shared/ui/Error/Error";
import MentorCard from "./MentorCard";
import SkeletonMentorCard from "./SkeletonMentorCard";
const MentorList: React.FC = () => {
const { data: mentors, isLoading, error } = useMentors();
if (isLoading) {
return (
<main className="mentors-grid">
<SkeletonMentorCard />
<SkeletonMentorCard />
<SkeletonMentorCard />
<SkeletonMentorCard />
<SkeletonMentorCard />
<SkeletonMentorCard />
</main>
);
}
if (error) return <Error />;
if (!mentors || mentors.length === 0) return <p>No mentors available.</p>;
return (
<>
<main className="mentors-grid">
{mentors.map((mentor) => (
<MentorCard key={mentor.id} {...mentor} />
))}
</main>
</>
);
}
export default MentorList;
let’s wrap the mentors feature folder (frontend/src/features/mentors/) with a clean index barrel so everything exports in a predictable, maintainable way. This way, consumers only import from the folder, not individual files.
import styles from './SkeletonMentorCard.module.css';
const SkeletonMentorCard: React.FC = () => {
return (
<article className={styles['mentor-card']}>
<div className={styles['mentor-avatar']} />
<div className={styles['mentor-name']} />
<div className={styles['mentor-title']} />
<div className={styles['mentor-expertise']} />
<div className={styles['mentor-link']} />
</article>
);
};
export default SkeletonMentorCard;
We get back 1 step and go next folder layouts so we in frontend/src/layouts
/MainLayout.tsx got :
import { Outlet } from "react-router";
import MainNavbar from "./MainNavbar";
const MainLayout: React.FC = () => {
return (
<>
<MainNavbar />
<div className="page-container">
<Outlet />
</div>
</>
);
}
export default MainLayout;
And for navigation, we got frontend/src/layouts
/MainNavbar.tsx :
import React from 'react';
import { Link } from 'react-router';
const MainNavbar: React.FC = () => {
return (
<nav className="navbar">
<div className="nav-container">
<Link to={'/'} className="nav-logo">MentorHub</Link>
</div>
</nav>
);
};
export default MainNavbar;
And we end this part with "types" ,frontend/src/types
/mentor.ts
export type Mentor = {
id: number;
fullName: string;
email: string | null;
title: string;
bio: string | null;
technicalBio: string | null;
mentoringStyle: string | null;
audience: string | null;
avatar: string | null;
expertise: string[];
availability: 'open' | 'limited' | 'full' | 'paused';
joined_at: string;
};
Top comments (0)