feat: implement frontend for calendar integration (Phase 3 & 4)

- Created MeetingSelection component for choosing between multiple active meetings
- Shows both active meetings and upcoming calendar events (30 min ahead)
- Displays meeting metadata with privacy controls (owner-only details)
- Supports creation of unscheduled meetings alongside calendar meetings

- Added waiting page for users joining before scheduled start time
- Shows countdown timer until meeting begins
- Auto-transitions to meeting when calendar event becomes active
- Handles early joining with proper routing

- Created collapsible info panel showing meeting details
- Displays calendar metadata (title, description, attendees)
- Shows participant count and duration
- Privacy-aware: sensitive info only visible to room owners

- Integrated ICS settings into room configuration dialog
- Test connection functionality with immediate feedback
- Manual sync trigger with detailed results
- Shows last sync time and ETag for monitoring
- Configurable sync intervals (1 min to 1 hour)

- New /room/{roomName} route for meeting selection
- Waiting room at /room/{roomName}/wait?eventId={id}
- Classic room page at /{roomName} with meeting info
- Uses sessionStorage to pass selected meeting between pages

- Added new endpoints for active/upcoming meetings
- Regenerated TypeScript client with latest OpenAPI spec
- Proper error handling and loading states
- Auto-refresh every 30 seconds for live updates

- Color-coded badges for meeting status
- Attendee status indicators (accepted/declined/tentative)
- Responsive design with Chakra UI components
- Clear visual hierarchy between active and upcoming meetings
- Smart truncation for long attendee lists

This completes the frontend implementation for calendar integration,
enabling users to seamlessly join scheduled meetings from their
calendar applications.
This commit is contained in:
2025-08-18 19:29:56 -06:00
parent 29725ee72d
commit 575f20fee2
12 changed files with 5606 additions and 12 deletions

View File

@@ -24,10 +24,13 @@ import { notFound } from "next/navigation";
import { useRecordingConsent } from "../recordingConsentContext";
import { useMeetingAudioConsent } from "../lib/apiHooks";
import type { components } from "../reflector-api";
import useApi from "../lib/useApi";
import { FaBars, FaInfoCircle } from "react-icons/fa6";
import MeetingInfo from "./MeetingInfo";
import { useAuth } from "../lib/AuthProvider";
type Meeting = components["schemas"]["Meeting"];
import { FaBars } from "react-icons/fa6";
import { useAuth } from "../lib/AuthProvider";
type Room = components["schemas"]["Room"];
export type RoomDetails = {
params: {
@@ -263,6 +266,9 @@ export default function Room(details: RoomDetails) {
const status = useAuth().status;
const isAuthenticated = status === "authenticated";
const isLoading = status === "loading" || meeting.loading;
const [showMeetingInfo, setShowMeetingInfo] = useState(false);
const [room, setRoom] = useState<Room | null>(null);
const api = useApi();
const roomUrl = meeting?.response?.host_room_url
? meeting?.response?.host_room_url
@@ -276,6 +282,15 @@ export default function Room(details: RoomDetails) {
router.push("/browse");
}, [router]);
// Fetch room details
useEffect(() => {
if (!api || !roomName) return;
api.v1RoomsRetrieve({ roomName }).then(setRoom).catch(console.error);
}, [api, roomName]);
const isOwner = session?.user?.id === room?.user_id;
useEffect(() => {
if (
!isLoading &&
@@ -327,6 +342,25 @@ export default function Room(details: RoomDetails) {
wherebyRef={wherebyRef}
/>
)}
{meeting?.response && (
<>
<Button
position="absolute"
top="56px"
right={showMeetingInfo ? "320px" : "8px"}
zIndex={1000}
colorPalette="blue"
size="sm"
onClick={() => setShowMeetingInfo(!showMeetingInfo)}
leftIcon={<Icon as={FaInfoCircle} />}
>
Meeting Info
</Button>
{showMeetingInfo && (
<MeetingInfo meeting={meeting.response} isOwner={isOwner} />
)}
</>
)}
</>
)}
</>