Skip to main content

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. 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
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 logo
  • Service description
  • Contact phone number

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">
                      &quot;{t.content}&quot;
                    </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:
3 columns for screens wider than 1100px
Custom CSS for Masonry:
globals.css
.my-masonry-grid {
  display: flex;
  margin-left: -6px;
  width: auto;
}

.my-masonry-grid_column {
  padding-left: 8px;
  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

EmblaCarouselArrowButton

Custom hook and components for Embla Carousel navigation. Location: components/custom/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:
prevBtnDisabled
boolean
Disables previous button when at first slide
nextBtnDisabled
boolean
Disables next button when at last slide
onPrevButtonClick
function
Handler for previous button click
onNextButtonClick
function
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

  • 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

Architecture

Learn about the overall website architecture

Deployment

Deployment and production configurations

Build docs developers (and LLMs) love