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
57 changes: 49 additions & 8 deletions src/components/MeetingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import {
PersonIcon as Users,
HomeIcon as MapPin,
DragHandleHorizontalIcon,
FileTextIcon,
GlobeIcon as Globe,
} from "@radix-ui/react-icons";
import type { Meeting, MeetingComment } from "../types";
import { cn } from "../lib/utils";
import { useSettingsStore } from "../stores/settingsStore";
import { MeetingContent } from "@/components/MeetingContent";
import { parseMeetingContent } from "@/utils/meetingContentFormatter";

interface MeetingCardProps {
meeting: Meeting;
Expand Down Expand Up @@ -104,6 +107,24 @@ export function MeetingCard({
});
};

const hasPreRead =
meeting.description &&
parseMeetingContent(meeting.description).preReadLinks.length > 0;

const isExternalMeeting = () => {
const filteredParticipants = meeting.participants.filter(
(p) => !p.includes("resource.calendar.google.com")
);

const domains = filteredParticipants
.map((email) => email.split("@")[1])
.filter(Boolean);

const uniqueDomains = new Set(domains);

return uniqueDomains.size > 1;
};

return (
<Card
className={cn(
Expand All @@ -117,6 +138,24 @@ export function MeetingCard({
<DragHandleHorizontalIcon className="w-4 h-4 text-gray-400 cursor-move" />
<span className="text-base font-semibold">#{displayRank}</span>
<span className="text-base">{meeting.title}</span>
{hasPreRead && (
<FileTextIcon
className="h-4 w-4 text-blue-600"
aria-label="Has pre-read materials"
/>
)}
{isExternalMeeting() && (
<Globe
className="h-4 w-4 text-purple-600"
aria-label="External meeting"
/>
)}
<div className="flex items-center">
<Users className="w-3 h-3 mr-1" />
<span className="text-sm text-gray-600">
{meeting.participants.length}
</span>
</div>
{getStatusCount() > 0 && (
<span className="bg-red-100 text-red-600 text-xs px-1.5 py-0.5 rounded-full">
{getStatusCount()} action{getStatusCount() !== 1 ? "s" : ""}{" "}
Expand Down Expand Up @@ -147,17 +186,19 @@ export function MeetingCard({

{isExpanded && (
<CardContent className="px-2 pb-2">
{meeting.description && (
<MeetingContent
content={meeting.description}
participants={meeting.participants}
/>
)}
<div className="mb-4 space-y-1 text-xs text-gray-600">
{meeting.description && (
<MeetingContent content={meeting.description} />
)}
<div className="flex items-center">
<MapPin className="w-3 h-3 mr-1" />
<span>{meeting.location}</span>
</div>
<div className="flex items-start">
<Users className="w-3 h-3 mr-1 mt-0.5" />
<span>{meeting.participants.join(", ")}</span>
<span className="text-sm">
{meeting.location.split("https://").shift()?.trim() ||
meeting.location}
</span>
</div>
</div>

Expand Down
200 changes: 135 additions & 65 deletions src/components/MeetingContent.tsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,150 @@
import { parseMeetingContent } from "@/utils/meetingContentFormatter";

interface MeetingContentProps {
content: string;
}
// Sub-components
const PreReadMaterials = ({
links,
}: {
links: Array<{ url: string; title: string }>;
}) => {
// Filter out Zoom links
const filteredLinks = links.filter(
(link) => !link.url.toLowerCase().includes("zoom")
);

export function MeetingContent({ content }: MeetingContentProps) {
const formattedContent = parseMeetingContent(content);
if (filteredLinks.length === 0) return null;

return (
<div className="space-y-4">
<h3 className="font-medium">{formattedContent.title}</h3>

{formattedContent.preReadLinks.length > 0 && (
<div className="space-y-1">
<h4 className="text-sm font-medium">Pre-read Materials:</h4>
<ul className="text-sm space-y-1">
{formattedContent.preReadLinks.map((link, index) => (
<li key={index}>
<a
href={link.url}
className="text-blue-600 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
{link.title}
</a>
</li>
))}
</ul>
</div>
)}

{formattedContent.attendees.length > 0 && (
<div className="space-y-1">
<h4 className="text-sm font-medium">Attendees:</h4>
<p className="text-sm text-gray-600">
{formattedContent.attendees.join(", ")}
</p>
</div>
)}

{formattedContent.joinUrl && (
<div className="space-y-2">
<div className="flex space-x-2">
<div className="space-y-1 bg-blue-50 p-3 rounded-md">
<h4 className="text-sm font-medium text-blue-800">Pre-read Materials:</h4>
<ul className="text-sm space-y-1">
{filteredLinks.map((link, index) => (
<li key={index}>
<a
href={formattedContent.joinUrl}
className="text-blue-600 hover:underline text-sm"
href={link.url}
className="text-blue-600 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
Join Zoom Meeting
{link.title || "Agenda"}
</a>
</div>
{formattedContent.meetingId && (
<p className="text-sm text-gray-600">
Meeting ID: {formattedContent.meetingId}
</p>
)}
{formattedContent.passcode && (
<p className="text-sm text-gray-600">
Passcode: {formattedContent.passcode}
</p>
)}
</div>
)}
</li>
))}
</ul>
</div>
);
};

const AgendaSection = ({ agenda }: { agenda: string }) => {
if (!agenda) return null;

return (
<div className="space-y-1 bg-green-50 p-3 rounded-md">
<h4 className="text-sm font-medium text-green-800">Agenda</h4>
<div className="text-sm text-green-900">
{agenda.replace(/^Agenda:\s*/i, "")}
</div>
</div>
);
};

const Attendees = ({ participants }: { participants: string[] }) => {
if (participants.length === 0) return null;

{formattedContent.phoneNumbers.length > 0 && (
<div className="text-xs text-gray-500 space-y-1">
<h4 className="font-medium">Dial-in Numbers:</h4>
<div className="space-y-0.5">
{formattedContent.phoneNumbers.map((number, index) => (
<p key={index}>{number}</p>
))}
</div>
</div>
return (
<div className="space-y-1 bg-gray-50 p-3 rounded-md">
<h4 className="text-sm font-medium">Attendees:</h4>
<div className="flex flex-wrap gap-2">
{participants.map((participant, index) => (
<span
key={index}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-sm bg-gray-100 text-gray-800"
>
{participant}
</span>
))}
</div>
</div>
);
};

const ZoomInfo = ({
joinUrl,
meetingId,
passcode,
}: {
joinUrl: string;
meetingId: string;
passcode: string;
}) => {
if (!joinUrl) return null;

return (
<div className="space-y-2">
<div className="flex space-x-2">
<a
href={joinUrl}
className="text-blue-600 hover:underline text-sm"
target="_blank"
rel="noopener noreferrer"
>
Join Zoom Meeting
</a>
</div>
{meetingId && (
<p className="text-sm text-gray-600">Meeting ID: {meetingId}</p>
)}
{passcode && (
<p className="text-sm text-gray-600">Passcode: {passcode}</p>
)}
</div>
);
};

// Main component
interface MeetingContentProps {
content: string;
participants: string[];
}

export function MeetingContent({ content, participants }: MeetingContentProps) {
const formattedContent = parseMeetingContent(content);
const filteredParticipants = participants.filter(
(participant) => !participant.includes("resource.calendar.google.com")
);

// Create a linked title when the title is an anchor tag
const TitleContent = () => {
if (formattedContent.title.startsWith("<a href=")) {
const link = formattedContent.preReadLinks[0];
if (link) {
return (
<a
href={link.url}
className="font-medium text-blue-600 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
{link.title || "Meeting"}
</a>
);
}
}
return <span>{formattedContent.title}</span>;
};

return (
<div className="space-y-4">
<h3 className="font-medium">
<TitleContent />
</h3>
<PreReadMaterials links={formattedContent.preReadLinks} />
<AgendaSection agenda={formattedContent.agenda} />
<Attendees participants={filteredParticipants} />
<ZoomInfo
joinUrl={formattedContent.joinUrl}
meetingId={formattedContent.meetingId}
passcode={formattedContent.passcode}
/>
</div>
);
}
Loading
Loading