Component Documentation
This page documents all custom components used throughout the Yemira Services Nettoyages website. All components are built with React 19, TypeScript, and Tailwind CSS.
Navigation Components
NavBar Component
The navigation bar component displays the company logo and a call-to-action button. It’s fixed on desktop and responsive on mobile.
Location: components/custom/NavBar.tsx
import Image from 'next/image' ;
import React from 'react' ;
import brand from '@/assets/images/brand.png' ;
import { Button } from '../ui/button' ;
import Link from 'next/link' ;
export default function NavBar () {
return (
< div className = "bg-white md:shadow-lg md:p-2 shadow-slate-700/5 md:fixed w-full z-10 relative" >
< div className = "md:flex justify-between items-center max-w-6xl mx-auto hidden" >
< Image
src = { brand }
alt = "Logo représentant une entreprise de nettoyage"
height = { 100 }
width = { 100 }
/>
< Button
className = "rounded-full cursor-pointer p-6 text-md font-heading text-md"
asChild
>
< Link href = { 'https://wa.link/wa3emf' } > 05 05 40 48 66 </ Link >
</ Button >
</ div >
</ div >
);
}
Features:
Fixed positioning on desktop (z-index: 10)
Hidden on mobile (logo appears in hero section instead)
WhatsApp link integration
Shadow and responsive layout
Max-width container (6xl / 1280px)
Props: None (no props required)
Uses Next.js Image component for optimized images
Uses shadcn/ui Button component with asChild prop
Responsive classes: md: prefix for desktop-only styles
Custom font class: font-heading for button text
Comprehensive footer with company information, site map, social links, and contact options.
Location: components/custom/Footer.tsx
Footer.tsx (Key Sections)
import Image from 'next/image' ;
import React from 'react' ;
import logo from '@/assets/images/logo.png' ;
import brand from '@/assets/images/brand.png' ;
import Link from 'next/link' ;
import { Button } from '../ui/button' ;
export default function Footer () {
return (
< div className = " space-y-24" >
< div className = "grid md:grid-cols-4 md:gap-4 gap-12 max-w-6xl mx-auto md:px-0 px-4" >
{ /* Company Info */ }
< div className = "space-y-2" >
< Image
src = { brand }
alt = "Logo représentant une entreprise de nettoyage"
height = { 150 }
width = { 150 }
/>
< p className = "leading-8 text-slate-400" >
Nettoyage en profondeur de logements, hôtels, restaurants,
entrepôts, centres commerciaux, entre autres partout à Abidjan.
</ p >
< p className = "text-slate-400 flex flex-col gap-1" >
< span className = "Capitalize " > Contact: </ span >
< span > +225 05 05 40 48 66 </ span >
</ p >
</ div >
{ /* Site Map */ }
< div className = "space-y-8" >
< h1 className = "font-button text-xl" > Plan du Site </ h1 >
< ul className = "flex flex-col gap-4" >
< Link href = { '#' } className = "text-slate-400 capitalize" >
commencer
</ Link >
{ /* More links... */ }
</ ul >
</ div >
{ /* Social Links */ }
< div className = "space-y-8" >
< h1 className = "font-button text-xl" > Suivez-nous </ h1 >
< ul className = "flex flex-col gap-4" >
< Link
href = { 'https://www.facebook.com/yemiraservices' }
className = "text-slate-400 capitalize"
>
Facebook
</ Link >
< Link
href = { 'https://wa.link/wa3emf' }
className = "text-slate-400 capitalize"
>
whatsapp
</ Link >
</ ul >
</ div >
{ /* CTA */ }
< div className = "space-y-8" >
< h1 className = "font-button text-xl" > Ecrivez-nous </ h1 >
< ul className = "flex flex-col gap-4" >
< Button
asChild
className = "rounded-full cursor-pointer p-6 text-md font-heading text-md w-fit"
>
< Link href = { 'https://wa.link/wa3emf' } className = " capitalize" >
Whatsapp
</ Link >
</ Button >
</ ul >
</ div >
</ div >
{ /* Copyright Bar */ }
< div className = "bg-primary" >
< div className = "text-white max-w-6xl mx-auto py-4" >
< p className = "text-sm md:text-left text-center" >
Yemira Service Abidjan - {new Date (). getFullYear () }
</ p >
</ div >
</ div >
</ div >
);
}
Footer Sections:
Company Info
Site Map
Social Media
CTA
Company logo
Service description
Contact phone number
Navigation links
Internal page references
Section anchors
Facebook link
WhatsApp link
Social integration
WhatsApp contact button
Direct call-to-action
Interactive Components
Testimonials Component
Carousel component displaying customer testimonials with Embla Carousel integration.
Location: components/custom/Testimonials.tsx
'use client' ;
import useEmblaCarousel from 'embla-carousel-react' ;
import React from 'react' ;
import { usePrevNextButtons } from './EmblaCarouselArrowButton' ;
import { ArrowLeft , ArrowRight } from 'lucide-react' ;
import { cn } from '@/lib/utils' ;
const TestimonialsList = [
{
content:
"Le service était excellent et l'équipe très aimable et attentionnée. Je suis très satisfaite du service, merci à toute l'équipe de Deep Cleaning." ,
username: 'sofia kouassi' ,
type: 'Résidentiel' ,
},
// ... more testimonials
];
export default function Testimonials () {
const [ emblaRef , emblaApi ] = useEmblaCarousel ({
align: 'center' ,
dragFree: false ,
containScroll: undefined ,
});
const {
onPrevButtonClick ,
onNextButtonClick ,
prevBtnDisabled ,
nextBtnDisabled ,
} = usePrevNextButtons ( emblaApi );
return (
< div className = "bg-primary text-white" >
< div className = "py-24 space-y-8" >
{ /* Header */ }
< div className = "flex flex-col gap-3 items-center max-w-2xl mx-auto" >
< h1 className = "text-5xl font-heading text-center md:text-left" >
Ce que disent nos clients
</ h1 >
< p className = "text-center md:px-0 px-4 text-lg" >
Pour nous, la satisfaction de nos clients est notre plus grande
source de joie...
</ p >
</ div >
{ /* Carousel */ }
< div className = "relative" >
< div className = "embla" ref = { emblaRef } >
< div className = "embla__container" >
{ TestimonialsList . map (( t , index ) => (
< div className = "embla__slide max-w-4xl m-auto p-4" key = { index } >
< div className = "flex flex-col justify-center items-center h-88 rounded-lg p-12 gap-12 bg-white" >
< p className = "text-slate-400 md:text-lg text-center" >
"{ t . content }"
</ p >
{ /* User Avatar & Info */ }
< div className = "flex gap-2" >
< div className = "size-12 rounded-full relative overflow-hidden" >
< img
className = "absolute size-full object-cover"
src = { `https://api.dicebear.com/9.x/glass/svg?seed= ${ t . username . trim () } ` }
alt = "resident client"
width = { 900 }
height = { 900 }
/>
</ div >
< div className = "flex flex-col" >
< h1 className = "capitalize text-black font-semibold" >
{ t . username }
</ h1 >
< p className = "text-slate-400" > { t . type } </ p >
</ div >
</ div >
</ div >
</ div >
)) }
</ div >
</ div >
{ /* Navigation Buttons */ }
< div className = "absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-4xl mx-auto flex flex-row-reverse justify-between items-center h-full" >
< button
disabled = { nextBtnDisabled }
onClick = { onNextButtonClick }
className = { cn (
'bg-white drop-shadow-2xl size-fit rounded-full p-3 transition-colors duration-200 hover:bg-gray-100 active:scale-95' ,
{ 'opacity-50 pointer-events-none' : nextBtnDisabled }
) }
>
< ArrowRight className = "text-black" />
</ button >
< button
disabled = { prevBtnDisabled }
onClick = { onPrevButtonClick }
className = { cn (
'bg-white drop-shadow-2xl size-fit rounded-full p-3 transition-colors duration-200 hover:bg-gray-100 active:scale-95' ,
{ 'opacity-50 pointer-events-none' : prevBtnDisabled }
) }
>
< ArrowLeft className = "text-black" />
</ button >
</ div >
</ div >
</ div >
</ div >
);
}
Key Features:
Client-Side Uses 'use client' directive for interactivity
Embla Carousel Smooth carousel with touch/drag support
Dynamic Avatars DiceBear API for unique user avatars
Responsive Adapts to mobile and desktop layouts
Embla Configuration:
const [ emblaRef , emblaApi ] = useEmblaCarousel ({
align: 'center' , // Center active slide
dragFree: false , // Snap to slides
containScroll: undefined
});
This is a client component - it requires JavaScript to function and uses browser APIs.
Photos Component
Masonry layout gallery showcasing project photos.
Location: components/custom/Photos.tsx
'use client' ;
import React from 'react' ;
import Image from 'next/image' ;
import Masonry from 'react-masonry-css' ;
import { Button } from '../ui/button' ;
import Link from 'next/link' ;
import image_1 from '@/assets/images/1.jpg' ;
import image_2 from '@/assets/images/2.jpg' ;
// ... more imports
const breakpointColumnsObj = {
default: 3 ,
1100 : 2 ,
700 : 2 ,
};
export default function Photos () {
const images = [
image_1 ,
image_2 ,
image_3 ,
// ... 11 images total
];
return (
< div className = "py-28 space-y-16" >
{ /* Header */ }
< div className = "flex flex-col gap-3 items-center max-w-2xl mx-auto" >
< h1 className = "text-5xl font-heading text-center md:text-left" >
Des Projets < span className = "text-primary" > irréprochables </ span >
</ h1 >
< p className = "text-center md:px-0 px-4 md:text-lg text-slate-400" >
Un travail de qualité se doit d'être démontré...
</ p >
</ div >
{ /* Masonry Grid */ }
< div className = "p-4" >
< Masonry
breakpointCols = { breakpointColumnsObj }
className = "my-masonry-grid"
columnClassName = "my-masonry-grid_column"
>
{ images . map (( src , i ) => (
< Image
key = { i }
src = { src }
alt = { `image ${ i } ` }
className = "mb-4 w-full"
width = { 900 }
height = { 900 }
/>
)) }
</ Masonry >
</ div >
{ /* CTA Button */ }
< Button
asChild
className = "rounded-full cursor-pointer p-6 text-md font-heading text-md w-fit relative mx-auto flex justify-center"
>
< Link href = { 'tel:+2250505404866' } className = "" >
Contactez-nous
</ Link >
</ Button >
</ div >
);
}
Masonry Breakpoints:
Desktop (Default)
Tablet (1100px)
Mobile (700px)
3 columns for screens wider than 1100px
2 columns for screens between 700-1100px
2 columns for screens under 700px
Custom CSS for Masonry:
.my-masonry-grid {
display : flex ;
margin-left : -6 px ;
width : auto ;
}
.my-masonry-grid_column {
padding-left : 8 px ;
background-clip : padding-box ;
}
Whatsapp Component
Fixed WhatsApp floating button for instant customer contact.
Location: components/custom/Whatsapp.tsx
'use client' ;
import Link from 'next/link' ;
import React from 'react' ;
export default function Whatsapp () {
return (
< Link
href = { 'https://wa.link/wa3emf' }
className = "bg-linear-65 from-[#1FE9A3] to-[#00CF8E] w-fit fixed bottom-0 right-0 rounded-full p-2 z-20 m-4"
>
< svg
xmlns = "http://www.w3.org/2000/svg"
className = "bg-transparent md:size-12 size-10 rounded-full"
width = "100"
height = "100"
viewBox = "0 0 24 24"
>
< path
d = "M 12.011719 2 C 6.5057187 2 2.0234844 6.478375..."
fill = "#fff"
/>
</ svg >
</ Link >
);
}
Features:
Fixed Position Stays in bottom-right corner (z-index: 20)
Gradient Background WhatsApp brand colors with gradient
Responsive Size 48px on desktop, 40px on mobile
Direct Link Opens WhatsApp chat immediately
Utility Components
Custom hook and components for Embla Carousel navigation.
Location: components/custom/EmblaCarouselArrowButton.tsx
EmblaCarouselArrowButton.tsx
import React , {
ComponentPropsWithRef ,
useCallback ,
useEffect ,
useState ,
} from 'react' ;
import { EmblaCarouselType } from 'embla-carousel' ;
type UsePrevNextButtonsType = {
prevBtnDisabled : boolean ;
nextBtnDisabled : boolean ;
onPrevButtonClick : () => void ;
onNextButtonClick : () => void ;
};
export const usePrevNextButtons = (
emblaApi : EmblaCarouselType | undefined ,
onButtonClick ?: ( emblaApi : EmblaCarouselType ) => void
) : UsePrevNextButtonsType => {
const [ prevBtnDisabled , setPrevBtnDisabled ] = useState ( true );
const [ nextBtnDisabled , setNextBtnDisabled ] = useState ( true );
const onPrevButtonClick = useCallback (() => {
if ( ! emblaApi ) return ;
emblaApi . scrollPrev ();
if ( onButtonClick ) onButtonClick ( emblaApi );
}, [ emblaApi , onButtonClick ]);
const onNextButtonClick = useCallback (() => {
if ( ! emblaApi ) return ;
emblaApi . scrollNext ();
if ( onButtonClick ) onButtonClick ( emblaApi );
}, [ emblaApi , onButtonClick ]);
const onSelect = useCallback (( emblaApi : EmblaCarouselType ) => {
setPrevBtnDisabled ( ! emblaApi . canScrollPrev ());
setNextBtnDisabled ( ! emblaApi . canScrollNext ());
}, []);
useEffect (() => {
if ( ! emblaApi ) return ;
onSelect ( emblaApi );
emblaApi . on ( 'reInit' , onSelect ). on ( 'select' , onSelect );
}, [ emblaApi , onSelect ]);
return {
prevBtnDisabled ,
nextBtnDisabled ,
onPrevButtonClick ,
onNextButtonClick ,
};
};
// PrevButton and NextButton components also exported
Hook Features:
Disables previous button when at first slide
Disables next button when at last slide
Handler for previous button click
Handler for next button click
Utility Functions
cn() - Class Name Utility
Merges Tailwind CSS classes without conflicts.
Location: lib/utils.ts
import { clsx , type ClassValue } from 'clsx' ;
import { twMerge } from 'tailwind-merge' ;
export function cn ( ... inputs : ClassValue []) {
return twMerge ( clsx ( inputs ));
}
Usage Example:
const buttonClasses = cn (
'bg-white drop-shadow-2xl size-fit rounded-full p-3' ,
{ 'opacity-50 pointer-events-none' : isDisabled }
);
The cn() utility combines clsx for conditional classes and tailwind-merge to prevent style conflicts.
Component Best Practices
Server vs Client Components
Use Server Components by default (NavBar, Footer)
Use Client Components ('use client') only when needed for:
Interactive features (Testimonials, Photos)
Browser APIs
React hooks (useState, useEffect)
Always use Next.js Image component
Specify width and height for better LCP
Use descriptive alt text in French
Import images from assets directory
Mobile-first approach
Use Tailwind responsive prefixes (md:, lg:)
Test on multiple screen sizes
Hidden classes for mobile/desktop variants
Semantic HTML elements
ARIA labels where appropriate
Keyboard navigation support
Disabled states for buttons
Related Pages
Architecture Learn about the overall website architecture
Deployment Deployment and production configurations