enesdmc
➜ All Components

Meeting Card

Nov 1
·
2024

Weekly Call

Image

Product Design

I'd like to discuss our plans for the next quarter. I'll add the agenda later.

Image
Image
Image
+3

Mar 15

1:00 PM - 2:00 PM

40 min

Participants

    John Doe - Product Manager

    Image
    Image
    Image
    +3

    Jane Smith - UX Designer

    Image
    Image
    Image
    +3

    Sam Wilson - Developer

    Image
    Image
    Image
    +3

This MeetingCard component is crafted using Next.js and React, structured for optimal performance and smooth user interactions. It leverages Tailwind CSS for rapid, responsive styling and ensures type safety and clear data structures with TypeScript.

The component layout is enhanced by the Framer Motion library, which enables smooth, visually appealing animations for elements such as hover and click interactions, providing a more engaging experience. Lucide-react icons, like the Clock and X icons, add clean, scalable visuals for timing and closing functionality. The dark mode and light mode compatibility are supported with Next.js’s next-themes, ensuring seamless theme transitions.

The component displays meeting details using meetingDetails, an object containing structured information like title, description, agenda, date, and time. Participants are managed in participantsData, an array of participant information for easy listing and reordering. The drag-and-drop feature is enabled through @hello-pangea/dnd, which lets users reorder participant items dynamically by dragging, enhancing interactivity and flexibility.

In summary, this component brings together essential tools to create a modular, interactive, and visually appealing user experience, designed for modern web applications.

Installation

To install the Meeting Card component run:

Install
bun add framer-motion
bun add lucide-react
bun add @hello-pangea/dnd
@hello-pangea/dnd

The @hello-pangea/dnd package brings drag-and-drop functionality to the component, making the user experience more interactive and flexible. Here's a breakdown of how each part of the drag-and-drop setup contributes:


DragDropContext: Acts as the top-level wrapper to handle the entire drag-and-drop area. It monitors when a drag action starts, moves, and ends.


Droppable: Defines the specific area where draggable items can be dropped. In this component, it wraps the list of participants, allowing them to be reordered within the designated area.


Draggable: Used for each individual item (in this case, each participant). It provides properties and handles to make items movable, supporting drag events for reordering while maintaining smooth transitions.


Overall, @hello-pangea/dnd enhances the component by enabling users to rearrange participants intuitively, creating a more flexible and user-friendly experience.

Usage

App.tsx
"use client"
import Image from "next/image"
import { useState } from "react"
import { motion } from "framer-motion"
import { Clock, GripVertical, X } from "lucide-react"
import { DragDropContext, Draggable, Droppable } from "@hello-pangea/dnd"
 
 
// Define the participant item structure
interface ParticipantItem {
  id: string
  text: string
}
 
// Sample participant data
const participantsData: ParticipantItem[] = [
  { id: "1", text: "John Doe - Product Manager" },
  { id: "2", text: "Jane Smith - UX Designer" },
  { id: "3", text: "Sam Wilson - Developer" },
]
 
// Sample meeting details
const meetingDetails = {
  title: "Weekly Call",
  description: "Product Design",
  agenda:
    "I'd like to discuss our plans for the next quarter. I'll add the agenda later.",
  date: "Mar 15",
  time: "1:00 PM - 2:00 PM",
  duration: "40 min",
  image: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
}
 
// Sample participant images
const images = [
  "https://images.unsplash.com/photo-1580894732444-8ecded7900cd?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  "https://images.unsplash.com/photo-1556157382-97eda2d62296?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
  "https://images.unsplash.com/photo-1543060829-a0029874b174?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
]
 
export const MeetingCard = () => {
  const [participants, setParticipants] =
    useState<ParticipantItem[]>(participantsData)
 
    // Function to handle drag-and-drop reordering
  const onDragEnd = (result: any) => {
 
    if (!result.destination) return // If no destination, return
 
    const items = Array.from(participants) // Create a new array from participants
    const [reorderedItem] = items.splice(result.source.index, 1) // Remove the reordered item
    items.splice(result.destination.index, 0, reorderedItem) // Insert the reordered item at the destination index
 
    setParticipants(items) // Update the participants state with the reordered items
  }
 
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
      className="p-4 rounded-lg w-[300px] border border-[#E5E7EB] dark:border-[#2D3748] font-sans flex flex-col gap-4 bg-[#F3F4F6] dark:bg-[#1A202C]"
    >
      <div className="flex items-center justify-between">
        <p className="text-[#374151] dark:text-[#F7FAFC] font-medium">
          {meetingDetails.title}
        </p>
        <motion.div
          whileHover={{ scale: 1.1 }}
          whileTap={{ scale: 0.9 }}
          className="text-[#6B7280] dark:text-[#E2E8F0] cursor-pointer"
        >
          <X size={18} />
        </motion.div>
      </div>
      <div className="flex gap-3">
        <Image
          alt="Image"
          src={meetingDetails.image}
          width={100}
          height={300}
          className="rounded-xl"
        />
        <div className="flex flex-col">
          <h2 className="text-sm text-[#6B7280] dark:text-[#E2E8F0]">
            {meetingDetails.description}
          </h2>
          <p className="text-xs text-[#6B7280] dark:text-[#E2E8F0]">
            {meetingDetails.agenda}
          </p>
          <div className="flex mt-auto">
            <div className="relative size-6">
              <Image
                fill
                alt="Image"
                src={images[0]}
                className="rounded-full object-cover"
              />
            </div>
            <div className="relative size-6 right-2">
              <Image
                fill
                alt="Image"
                src={images[1]}
                className="rounded-full object-cover"
              />
            </div>
            <div className="relative size-6 right-4">
              <Image
                fill
                alt="Image"
                src={images[2]}
                className="rounded-full object-cover"
              />
            </div>
            <div className="relative size-6 right-6 flex items-center justify-center border border-[#E5E7EB] dark:border-[#2D3748] bg-[#F3F4F6] dark:bg-[#2D3748]/50 text-[#374151] dark:text-[#E2E8F0] font-semibold rounded-full text-[10px]">
              +3
            </div>
          </div>
        </div>
      </div>
      <motion.button
        whileHover={{ scale: 1.05 }}
        whileTap={{ scale: 0.95 }}
        className="bg-[#6B7280]/10 border border-[#E5E7EB] dark:border-[#2D3748] w-full py-2 rounded-md text-xs text-[#6B7280] dark:text-[#E2E8F0] font-medium"
      >
        Join Meeting
      </motion.button>
      <div className="flex gap-1">
        <p className="bg-[#6B7280]/10 border text-[#6B7280] dark:text-[#E2E8F0] border-[#E5E7EB] dark:border-[#2D3748] rounded-md text-xs p-1.5">
          {meetingDetails.date}
        </p>
        <p className="bg-[#6B7280]/10 border text-[#6B7280] dark:text-[#E2E8F0] border-[#E5E7EB] dark:border-[#2D3748] rounded-md text-xs p-1.5">
          {meetingDetails.time}
        </p>
        <p className="bg-[#6B7280]/10 border text-[#6B7280] dark:text-[#E2E8F0] border-[#E5E7EB] dark:border-[#2D3748] rounded-md text-xs p-1.5 flex gap-2 ml-auto">
          <Clock size={16} />
          {meetingDetails.duration}
        </p>
      </div>
      <div className="h-px w-full bg-[#E5E7EB] dark:bg-[#2D3748]" />
      <div>
        <h3 className="text-[#6B7280] dark:text-[#E2E8F0] font-medium">
          Participants
        </h3>
 
        <DragDropContext onDragEnd={onDragEnd}>
          <Droppable droppableId="participants">
            {(provided) => (
              <div {...provided.droppableProps} ref={provided.innerRef}>
                <ul>
                  {participants.map((participant, index) => (
                    <Draggable
                      key={participant.id}
                      draggableId={participant.id}
                      index={index}
                    >
                      {(provided) => (
                        <div
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                          ref={provided.innerRef}
                          className="bg-[#6B7280]/10 flex items-center border border-[#E5E7EB] dark:border-[#2D3748] rounded-md p-2 cursor-pointer mt-2"
                        >
                          <GripVertical className="size-3 text-[#6B7280] dark:text-[#E2E8F0]" />
                          <p className="text-xs text-[#6B7280] dark:text-[#E2E8F0]">
                            {participant.text}
                          </p>
                          <div className="relative flex ml-auto ">
                            <div className="absolute right-9 size-6">
                              <Image
                                fill
                                alt="Image"
                                src={images[0]}
                                className="rounded-full object-cover"
                              />
                            </div>
                            <div className="absolute right-6 size-6 ">
                              <Image
                                fill
                                alt="Image"
                                src={images[1]}
                                className="rounded-full object-cover"
                              />
                            </div>
                            <div className="absolute right-3 size-6 ">
                              <Image
                                fill
                                alt="Image"
                                src={images[2]}
                                className="rounded-full object-cover"
                              />
                            </div>
                            <div className="relative size-6 flex items-center justify-center border border-[#E5E7EB] dark:border-[#2D3748] bg-[#F3F4F6] dark:bg-[#2D3748]/50 text-[#374151] dark:text-[#E2E8F0] font-semibold rounded-full text-[10px]">
                              +3
                            </div>
                          </div>
                        </div>
                      )}
                    </Draggable>
                  ))}
                  {provided.placeholder} // Placeholder for the draggable area
                </ul>
              </div>
            )}
          </Droppable>
        </DragDropContext>
      </div>
    </motion.div>
  )
}