mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-22 13:19:05 +00:00
feat: reorganize room edit dialog and fix Force Sync button
- Move WebHook configuration from General to dedicated WebHook tab - Add WebHook tab after Share tab in room edit dialog - Fix Force Sync button not appearing by adding missing isEditing prop - Fix indentation issues in MeetingSelection component 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,356 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
Heading,
|
||||
Text,
|
||||
HStack,
|
||||
Badge,
|
||||
Spinner,
|
||||
Flex,
|
||||
Link,
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Wrap,
|
||||
} from "@chakra-ui/react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { FaSync, FaClock, FaUsers, FaEnvelope } from "react-icons/fa";
|
||||
import { LuArrowLeft } from "react-icons/lu";
|
||||
import {
|
||||
useRoomCalendarEvents,
|
||||
useRoomIcsSync,
|
||||
} from "../../../../lib/apiHooks";
|
||||
import type { components } from "../../../../reflector-api";
|
||||
|
||||
type CalendarEventResponse = components["schemas"]["CalendarEventResponse"];
|
||||
|
||||
export default function RoomCalendarPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const roomName = params.roomName as string;
|
||||
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
// React Query hooks
|
||||
const eventsQuery = useRoomCalendarEvents(roomName);
|
||||
const syncMutation = useRoomIcsSync();
|
||||
|
||||
const events = eventsQuery.data || [];
|
||||
const loading = eventsQuery.isLoading;
|
||||
const error = eventsQuery.error ? "Failed to load calendar events" : null;
|
||||
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
setSyncing(true);
|
||||
await syncMutation.mutateAsync({
|
||||
params: {
|
||||
path: { room_name: roomName },
|
||||
},
|
||||
});
|
||||
// Refetch events after sync
|
||||
await eventsQuery.refetch();
|
||||
} catch (err: any) {
|
||||
console.error("Sync failed:", err);
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatEventTime = (start: string, end: string) => {
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
};
|
||||
|
||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
};
|
||||
|
||||
const isSameDay = startDate.toDateString() === endDate.toDateString();
|
||||
|
||||
if (isSameDay) {
|
||||
return `${startDate.toLocaleDateString(undefined, dateOptions)} • ${startDate.toLocaleTimeString(undefined, options)} - ${endDate.toLocaleTimeString(undefined, options)}`;
|
||||
} else {
|
||||
return `${startDate.toLocaleDateString(undefined, dateOptions)} ${startDate.toLocaleTimeString(undefined, options)} - ${endDate.toLocaleDateString(undefined, dateOptions)} ${endDate.toLocaleTimeString(undefined, options)}`;
|
||||
}
|
||||
};
|
||||
|
||||
const isEventActive = (start: string, end: string) => {
|
||||
const now = new Date();
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
return now >= startDate && now <= endDate;
|
||||
};
|
||||
|
||||
const isEventUpcoming = (start: string) => {
|
||||
const now = new Date();
|
||||
const startDate = new Date(start);
|
||||
const hourFromNow = new Date(now.getTime() + 60 * 60 * 1000);
|
||||
return startDate > now && startDate <= hourFromNow;
|
||||
};
|
||||
|
||||
const getAttendeeDisplay = (attendee: any) => {
|
||||
// Use name if available, otherwise use email
|
||||
const displayName = attendee.name || attendee.email || "Unknown";
|
||||
// Extract just the name part if it's in "Name <email>" format
|
||||
const cleanName = displayName.replace(/<.*>/, "").trim();
|
||||
return cleanName;
|
||||
};
|
||||
|
||||
const getAttendeeEmail = (attendee: any) => {
|
||||
return attendee.email || "";
|
||||
};
|
||||
|
||||
const renderAttendees = (attendees: any[]) => {
|
||||
if (!attendees || attendees.length === 0) return null;
|
||||
|
||||
return (
|
||||
<HStack fontSize="sm" color="gray.600" flexWrap="wrap">
|
||||
<FaUsers />
|
||||
<Text>Attendees:</Text>
|
||||
<Wrap gap={2}>
|
||||
{attendees.map((attendee, index) => {
|
||||
const email = getAttendeeEmail(attendee);
|
||||
const display = getAttendeeDisplay(attendee);
|
||||
|
||||
if (email && email !== display) {
|
||||
return (
|
||||
<Tooltip
|
||||
key={index}
|
||||
content={
|
||||
<HStack>
|
||||
<FaEnvelope size="12" />
|
||||
<Text>{email}</Text>
|
||||
</HStack>
|
||||
}
|
||||
>
|
||||
<Badge variant="subtle" colorPalette="blue" cursor="help">
|
||||
{display}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Badge key={index} variant="subtle" colorPalette="blue">
|
||||
{display}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Wrap>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
const sortedEvents = [...events].sort(
|
||||
(a, b) =>
|
||||
new Date(a.start_time).getTime() - new Date(b.start_time).getTime(),
|
||||
);
|
||||
|
||||
// Separate events by status
|
||||
const now = new Date();
|
||||
const activeEvents = sortedEvents.filter((e) =>
|
||||
isEventActive(e.start_time, e.end_time),
|
||||
);
|
||||
const upcomingEvents = sortedEvents.filter(
|
||||
(e) => new Date(e.start_time) > now,
|
||||
);
|
||||
const pastEvents = sortedEvents
|
||||
.filter((e) => new Date(e.end_time) < now)
|
||||
.reverse();
|
||||
|
||||
return (
|
||||
<Box w={{ base: "full", md: "container.xl" }} mx="auto" pt={2}>
|
||||
<VStack align="stretch" gap={6}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack gap={3}>
|
||||
<IconButton
|
||||
aria-label="Back to rooms"
|
||||
title="Back to rooms"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => router.push("/rooms")}
|
||||
>
|
||||
<LuArrowLeft />
|
||||
</IconButton>
|
||||
<Heading size="lg">Calendar for {roomName}</Heading>
|
||||
</HStack>
|
||||
<Button colorPalette="blue" onClick={handleSync} disabled={syncing}>
|
||||
{syncing ? <Spinner size="sm" /> : <FaSync />}
|
||||
Force Sync
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{error && (
|
||||
<Box
|
||||
p={4}
|
||||
borderRadius="md"
|
||||
bg="red.50"
|
||||
borderLeft="4px solid"
|
||||
borderColor="red.400"
|
||||
>
|
||||
<Text fontWeight="semibold" color="red.800">
|
||||
Error
|
||||
</Text>
|
||||
<Text color="red.700">{error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<Flex justify="center" py={8}>
|
||||
<Spinner size="xl" />
|
||||
</Flex>
|
||||
) : events.length === 0 ? (
|
||||
<Box bg="white" borderRadius="lg" boxShadow="md" p={6}>
|
||||
<Text textAlign="center" color="gray.500">
|
||||
No calendar events found. Make sure your calendar is configured
|
||||
and synced.
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<VStack align="stretch" gap={6}>
|
||||
{/* Active Events */}
|
||||
{activeEvents.length > 0 && (
|
||||
<Box>
|
||||
<Heading size="md" mb={3} color="green.600">
|
||||
Active Now
|
||||
</Heading>
|
||||
<VStack align="stretch" gap={3}>
|
||||
{activeEvents.map((event) => (
|
||||
<Box
|
||||
key={event.id}
|
||||
bg="white"
|
||||
borderRadius="lg"
|
||||
boxShadow="md"
|
||||
p={6}
|
||||
borderColor="green.200"
|
||||
borderWidth={2}
|
||||
>
|
||||
<Flex justify="space-between" align="start">
|
||||
<VStack align="start" gap={2} flex={1}>
|
||||
<HStack>
|
||||
<Heading size="sm">
|
||||
{event.title || "Untitled Event"}
|
||||
</Heading>
|
||||
<Badge colorPalette="green">Active</Badge>
|
||||
</HStack>
|
||||
<HStack fontSize="sm" color="gray.600">
|
||||
<FaClock />
|
||||
<Text>
|
||||
{formatEventTime(
|
||||
event.start_time,
|
||||
event.end_time,
|
||||
)}
|
||||
</Text>
|
||||
</HStack>
|
||||
{event.description && (
|
||||
<Text fontSize="sm" color="gray.700" noOfLines={2}>
|
||||
{event.description}
|
||||
</Text>
|
||||
)}
|
||||
{renderAttendees(event.attendees)}
|
||||
</VStack>
|
||||
<Link href={`/${roomName}`}>
|
||||
<Button size="sm" colorPalette="green">
|
||||
Join Room
|
||||
</Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Upcoming Events */}
|
||||
{upcomingEvents.length > 0 && (
|
||||
<Box>
|
||||
<Heading size="md" mb={3}>
|
||||
Upcoming Events
|
||||
</Heading>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{upcomingEvents.map((event) => (
|
||||
<Card.Root key={event.id}>
|
||||
<Card.Body>
|
||||
<VStack align="start" spacing={2}>
|
||||
<HStack>
|
||||
<Heading size="sm">
|
||||
{event.title || "Untitled Event"}
|
||||
</Heading>
|
||||
{isEventUpcoming(event.start_time) && (
|
||||
<Badge colorPalette="orange">Starting Soon</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack fontSize="sm" color="gray.600">
|
||||
<FaClock />
|
||||
<Text>
|
||||
{formatEventTime(
|
||||
event.start_time,
|
||||
event.end_time,
|
||||
)}
|
||||
</Text>
|
||||
</HStack>
|
||||
{event.description && (
|
||||
<Text fontSize="sm" color="gray.700" noOfLines={2}>
|
||||
{event.description}
|
||||
</Text>
|
||||
)}
|
||||
{renderAttendees(event.attendees)}
|
||||
</VStack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Past Events */}
|
||||
{pastEvents.length > 0 && (
|
||||
<Box>
|
||||
<Heading size="md" mb={3} color="gray.500">
|
||||
Past Events
|
||||
</Heading>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{pastEvents.slice(0, 5).map((event) => (
|
||||
<Card.Root key={event.id} opacity={0.7}>
|
||||
<Card.Body>
|
||||
<VStack align="start" spacing={2}>
|
||||
<Heading size="sm">
|
||||
{event.title || "Untitled Event"}
|
||||
</Heading>
|
||||
<HStack fontSize="sm" color="gray.600">
|
||||
<FaClock />
|
||||
<Text>
|
||||
{formatEventTime(
|
||||
event.start_time,
|
||||
event.end_time,
|
||||
)}
|
||||
</Text>
|
||||
</HStack>
|
||||
{renderAttendees(event.attendees)}
|
||||
</VStack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
))}
|
||||
{pastEvents.length > 5 && (
|
||||
<Text fontSize="sm" color="gray.500" textAlign="center">
|
||||
And {pastEvents.length - 5} more past events...
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -7,11 +7,19 @@ import {
|
||||
IconButton,
|
||||
Text,
|
||||
Spinner,
|
||||
Badge,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { LuLink } from "react-icons/lu";
|
||||
import type { components } from "../../../reflector-api";
|
||||
import {
|
||||
useRoomActiveMeetings,
|
||||
useRoomUpcomingMeetings,
|
||||
} from "../../../lib/apiHooks";
|
||||
|
||||
type Room = components["schemas"]["Room"];
|
||||
type Meeting = components["schemas"]["Meeting"];
|
||||
type CalendarEventResponse = components["schemas"]["CalendarEventResponse"];
|
||||
import { RoomActionsMenu } from "./RoomActionsMenu";
|
||||
|
||||
interface RoomTableProps {
|
||||
@@ -63,6 +71,70 @@ const getZulipDisplay = (
|
||||
return "Enabled";
|
||||
};
|
||||
|
||||
function MeetingStatus({ roomName }: { roomName: string }) {
|
||||
const activeMeetingsQuery = useRoomActiveMeetings(roomName);
|
||||
const upcomingMeetingsQuery = useRoomUpcomingMeetings(roomName);
|
||||
|
||||
const activeMeetings = activeMeetingsQuery.data || [];
|
||||
const upcomingMeetings = upcomingMeetingsQuery.data || [];
|
||||
|
||||
if (activeMeetingsQuery.isLoading || upcomingMeetingsQuery.isLoading) {
|
||||
return <Spinner size="sm" />;
|
||||
}
|
||||
|
||||
if (activeMeetings.length > 0) {
|
||||
const meeting = activeMeetings[0];
|
||||
const title = (meeting.calendar_metadata as any)?.title || "Active Meeting";
|
||||
return (
|
||||
<VStack gap={1} alignItems="start">
|
||||
<Badge colorScheme="green" size="sm">
|
||||
Active
|
||||
</Badge>
|
||||
<Text fontSize="xs" color="gray.600" lineHeight={1}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500" lineHeight={1}>
|
||||
{meeting.num_clients} participants
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
if (upcomingMeetings.length > 0) {
|
||||
const event = upcomingMeetings[0];
|
||||
const startTime = new Date(event.start_time);
|
||||
const now = new Date();
|
||||
const diffMinutes = Math.floor(
|
||||
(startTime.getTime() - now.getTime()) / 60000,
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack gap={1} alignItems="start">
|
||||
<Badge colorScheme="orange" size="sm">
|
||||
{diffMinutes < 60 ? `In ${diffMinutes}m` : "Upcoming"}
|
||||
</Badge>
|
||||
<Text fontSize="xs" color="gray.600" lineHeight={1}>
|
||||
{event.title || "Scheduled Meeting"}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500" lineHeight={1}>
|
||||
{startTime.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
No meetings
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomTable({
|
||||
rooms,
|
||||
linkCopied,
|
||||
@@ -97,13 +169,16 @@ export function RoomTable({
|
||||
<Table.ColumnHeader width="250px" fontWeight="600">
|
||||
Room Name
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader width="250px" fontWeight="600">
|
||||
Zulip
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader width="150px" fontWeight="600">
|
||||
Room Size
|
||||
<Table.ColumnHeader width="200px" fontWeight="600">
|
||||
Current Meeting
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader width="200px" fontWeight="600">
|
||||
Zulip
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader width="120px" fontWeight="600">
|
||||
Room Size
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader width="150px" fontWeight="600">
|
||||
Recording
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader
|
||||
@@ -118,6 +193,9 @@ export function RoomTable({
|
||||
<Table.Cell>
|
||||
<Link href={`/${room.name}`}>{room.name}</Link>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<MeetingStatus roomName={room.name} />
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{getZulipDisplay(
|
||||
room.zulip_auto_post,
|
||||
|
||||
@@ -448,7 +448,7 @@ export default function RoomsList() {
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="calendar">Calendar</Tabs.Trigger>
|
||||
<Tabs.Trigger value="share">Share</Tabs.Trigger>
|
||||
<Tabs.Trigger value="webhook">Webhook</Tabs.Trigger>
|
||||
<Tabs.Trigger value="webhook">WebHook</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="general" pt={6}>
|
||||
|
||||
Reference in New Issue
Block a user