Weekly Call
I'd like to discuss our plans for the next quarter. I'll add the agenda later.
Mar 15
1:00 PM - 2:00 PM
40 min
John Doe - Product Manager
Jane Smith - UX Designer
Sam Wilson - Developer
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.
To install the Meeting Card component run:
bun add framer-motion
bun add lucide-react
bun add @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.
"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>
)
}