Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions client/src/components/meetings/EmptyState.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react";
import { Calendar, Search, FileText } from "lucide-react";

const EmptyState = ({ type }) => {
const states = {
noMeetings: {
icon: <Calendar size={64} className="text-gray-300" />,
title: "No Meetings Yet",
description: "You haven't uploaded any meetings yet. Start by uploading your first meeting to see it here.",
actionText: "Upload Your First Meeting",
actionLink: "/upload-meeting",
},
noResults: {
icon: <Search size={64} className="text-gray-300" />,
title: "No Meetings Found",
description: "We couldn't find any meetings matching your search or filters. Try adjusting your criteria.",
actionText: "Clear Filters",
actionLink: null,
},
noScheduled: {
icon: <FileText size={64} className="text-gray-300" />,
title: "No Scheduled Meetings",
description: "You don't have any scheduled meetings. Schedule a new meeting to get started.",
actionText: "Schedule a Meeting",
actionLink: "/create-meeting",
},
};

const state = states[type] || states.noMeetings;

return (
<div className="flex flex-col items-center justify-center py-20 px-4">
<div className="mb-6">{state.icon}</div>
<h3 className="text-2xl font-semibold text-gray-900 mb-3">{state.title}</h3>
<p className="text-gray-600 text-center max-w-md mb-8">{state.description}</p>
{state.actionLink && (
<a
href={state.actionLink}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium transition-colors"
>
{state.actionText}
</a>
)}
</div>
);
};

export default EmptyState;
190 changes: 190 additions & 0 deletions client/src/components/meetings/MeetingCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React, { useState } from "react";
import { Calendar, Clock, FileText, Tag, Trash2, Download, Edit2, Eye, MoreVertical } from "lucide-react";

const MeetingCard = ({ meeting, onDelete, onRename, onDownload }) => {
const [showMenu, setShowMenu] = useState(false);
const [isRenaming, setIsRenaming] = useState(false);
const [newTitle, setNewTitle] = useState(meeting.title);

const handleRenameSubmit = (e) => {
e.preventDefault();
if (newTitle.trim() && newTitle !== meeting.title) {
onRename(meeting._id, newTitle.trim());
}
setIsRenaming(false);
setNewTitle(meeting.title);
};
Comment thread
mrunmayeekokitkar marked this conversation as resolved.

const formatDate = (dateString) => {
if (!dateString) return "N/A";
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
};

const getStatusColor = (status) => {
switch (status?.toLowerCase()) {
case "completed":
return "bg-green-100 text-green-700";
case "processing":
return "bg-yellow-100 text-yellow-700";
case "failed":
return "bg-red-100 text-red-700";
default:
return "bg-gray-100 text-gray-700";
}
};

return (
<div className="bg-white rounded-xl shadow-md hover:shadow-lg transition-shadow duration-200 overflow-hidden border border-gray-100">
{/* Card Header */}
<div className="p-5 border-b border-gray-100">
<div className="flex items-start justify-between gap-3">
{isRenaming ? (
<form onSubmit={handleRenameSubmit} className="flex-1">
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
onBlur={() => {
setIsRenaming(false);
setNewTitle(meeting.title);
}}
autoFocus
className="w-full px-3 py-2 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-400 outline-none"
/>
</form>
) : (
<h3 className="text-lg font-semibold text-gray-900 line-clamp-2 flex-1">
{meeting.title || "Untitled Meeting"}
</h3>
)}
<div className="relative">
<button
onClick={() => setShowMenu(!showMenu)}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
>
<MoreVertical size={18} className="text-gray-500" />
</button>
{showMenu && (
<div className="absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-10 min-w-[160px]">
<button
onClick={() => {
setIsRenaming(true);
setShowMenu(false);
}}
className="w-full px-4 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-sm"
>
<Edit2 size={16} className="text-gray-500" />
Rename
</button>
<button
onClick={() => onDownload(meeting)}
className="w-full px-4 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-sm"
>
<Download size={16} className="text-gray-500" />
Download
</button>
Comment thread
mrunmayeekokitkar marked this conversation as resolved.
<button
onClick={() => {
onDelete(meeting._id);
setShowMenu(false);
}}
className="w-full px-4 py-2 text-left hover:bg-red-50 text-red-600 flex items-center gap-2 text-sm"
>
<Trash2 size={16} />
Delete
</button>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 mt-2">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(meeting.status)}`}>
{meeting.status || "Unknown"}
</span>
{meeting.meetingType && (
<span className="px-2 py-1 bg-blue-50 text-blue-700 rounded-full text-xs font-medium">
{meeting.meetingType}
</span>
)}
</div>
</div>

{/* Card Body */}
<div className="p-5 space-y-3">
{/* Date and Duration */}
<div className="flex items-center gap-4 text-sm text-gray-600">
<div className="flex items-center gap-1.5">
<Calendar size={16} className="text-gray-400" />
<span>{formatDate(meeting.date || meeting.createdAt)}</span>
</div>
{meeting.duration && (
<div className="flex items-center gap-1.5">
<Clock size={16} className="text-gray-400" />
<span>{meeting.duration} min</span>
</div>
)}
</div>

{/* Summary Preview */}
{meeting.summary && (
<div className="flex items-start gap-2">
<FileText size={16} className="text-gray-400 mt-0.5 flex-shrink-0" />
<p className="text-sm text-gray-600 line-clamp-3">
{meeting.summary}
</p>
</div>
)}

{/* Tags */}
{meeting.tags && meeting.tags.length > 0 && (
<div className="flex items-start gap-2">
<Tag size={16} className="text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex flex-wrap gap-1.5">
{meeting.tags.slice(0, 3).map((tag, index) => (
<span
key={index}
className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs"
>
{tag}
</span>
))}
{meeting.tags.length > 3 && (
<span className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs">
+{meeting.tags.length - 3}
</span>
)}
</div>
</div>
)}

{/* Participants */}
{meeting.participants && meeting.participants.length > 0 && (
<div className="text-sm text-gray-600">
<span className="font-medium">{meeting.participants.length}</span>
<span className="text-gray-500"> participant(s)</span>
</div>
)}
</div>

{/* Card Footer */}
<div className="px-5 py-3 bg-gray-50 border-t border-gray-100 flex items-center justify-between">
<span className="text-xs text-gray-500">
Created {formatDate(meeting.createdAt)}
</span>
<button
onClick={() => onDownload(meeting)}
className="text-sm text-blue-600 hover:text-blue-800 font-medium flex items-center gap-1"
>
<Eye size={16} />
View Details
</button>
</div>
Comment thread
mrunmayeekokitkar marked this conversation as resolved.
</div>
);
};

export default MeetingCard;
135 changes: 135 additions & 0 deletions client/src/components/meetings/MeetingFilters.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from "react";
import { Filter, ChevronDown, X } from "lucide-react";

const MeetingFilters = ({
filters,
onFilterChange,
onClearFilters,
}) => {
Comment thread
mrunmayeekokitkar marked this conversation as resolved.
const [showDropdown, setShowDropdown] = React.useState(false);

const filterOptions = [
{
key: "status",
label: "Processing Status",
options: [
{ value: "all", label: "All Statuses" },
{ value: "completed", label: "Completed" },
{ value: "processing", label: "Processing" },
{ value: "failed", label: "Failed" },
],
},
{
key: "meetingType",
label: "Meeting Type",
options: [
{ value: "all", label: "All Types" },
{ value: "conference", label: "Conference" },
{ value: "policy", label: "Policy" },
{ value: "event", label: "Event" },
{ value: "internal", label: "Internal" },
],
},
{
key: "dateRange",
label: "Date Range",
options: [
{ value: "all", label: "All Time" },
{ value: "today", label: "Today" },
{ value: "week", label: "Last 7 Days" },
{ value: "month", label: "Last 30 Days" },
{ value: "year", label: "Last Year" },
],
},
{
key: "sortBy",
label: "Sort By",
options: [
{ value: "createdAt-desc", label: "Newest First" },
{ value: "createdAt-asc", label: "Oldest First" },
{ value: "date-desc", label: "Meeting Date (Newest)" },
{ value: "date-asc", label: "Meeting Date (Oldest)" },
{ value: "title-asc", label: "Title (A-Z)" },
{ value: "title-desc", label: "Title (Z-A)" },
],
},
];

const hasActiveFilters =
filters.status !== "all" ||
filters.meetingType !== "all" ||
filters.dateRange !== "all" ||
filters.sortBy !== "createdAt-desc";

return (
<div className="relative">
<button
onClick={() => setShowDropdown(!showDropdown)}
className={`flex items-center gap-2 px-4 py-2.5 rounded-lg border transition-all ${
hasActiveFilters
? "bg-blue-50 border-blue-300 text-blue-700"
: "bg-white border-gray-300 text-gray-700 hover:border-gray-400"
}`}
>
<Filter size={18} />
<span className="font-medium">Filters</span>
{hasActiveFilters && (
<span className="bg-blue-600 text-white text-xs rounded-full px-2 py-0.5">
{Object.values(filters).filter((v) => v !== "all").length}
</span>
)}
Comment thread
mrunmayeekokitkar marked this conversation as resolved.
<ChevronDown size={16} className={`transition-transform ${showDropdown ? "rotate-180" : ""}`} />
</button>

{showDropdown && (
<div className="absolute right-0 top-full mt-2 bg-white rounded-xl shadow-lg border border-gray-200 p-4 z-50 w-80 max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-900">Filter Meetings</h3>
{hasActiveFilters && (
<button
onClick={() => {
onClearFilters();
setShowDropdown(false);
}}
className="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1"
>
<X size={14} />
Clear All
</button>
)}
</div>

<div className="space-y-4">
{filterOptions.map((filter) => (
<div key={filter.key}>
<label className="block text-sm font-medium text-gray-700 mb-2">
{filter.label}
</label>
<select
value={filters[filter.key]}
onChange={(e) => onFilterChange(filter.key, e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none text-sm"
>
{filter.options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
))}
</div>

<button
onClick={() => setShowDropdown(false)}
className="w-full mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium text-sm"
>
Apply Filters
</button>
</div>
)}
</div>
);
};

export default MeetingFilters;
Loading
Loading