diff --git a/package.json b/package.json
index cf5b050..429c3ca 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.12",
+ "@radix-ui/react-tooltip": "^1.2.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a9facf8..f18c3a4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -35,6 +35,9 @@ importers:
'@radix-ui/react-tabs':
specifier: ^1.1.12
version: 1.1.12(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.2.7
+ version: 1.2.7(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -663,6 +666,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-tooltip@1.2.7':
+ resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
@@ -3147,6 +3163,25 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.8
+ '@radix-ui/react-tooltip@1.2.7(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-popper': 1.2.7(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-portal': 1.1.9(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.8)(react@19.1.0)':
dependencies:
react: 19.1.0
diff --git a/src/app/conferences/[id]/page.jsx b/src/app/conferences/[id]/page.jsx
new file mode 100644
index 0000000..f1550d5
--- /dev/null
+++ b/src/app/conferences/[id]/page.jsx
@@ -0,0 +1,28 @@
+import Header from '@/components/header';
+import Footer from '@/components/footer';
+import ConferenceDetails from '@/components/conferences/conference-details';
+import { Suspense } from 'react';
+
+// This tells Next.js to allow pages to be generated on-demand
+// for params not returned by generateStaticParams.
+export const dynamicParams = true;
+
+export async function generateStaticParams() {
+ // Since conference rooms are dynamic and may require authentication to list,
+ // we can't statically generate them at build time.
+ // Returning an empty array will prevent build errors, and with dynamicParams = true,
+ // pages will be generated on-demand when visited.
+ return [];
+}
+
+export default function ConferenceDetailsPage({ params }) {
+ return (
+
{post.author.role}
{post.summary}
+{post.summary}
Read more → diff --git a/src/components/conferences/conference-details.jsx b/src/components/conferences/conference-details.jsx new file mode 100644 index 0000000..0db5819 --- /dev/null +++ b/src/components/conferences/conference-details.jsx @@ -0,0 +1,231 @@ +"use client"; + +import { useState, useEffect, useCallback } from 'react'; +import { useAuth } from '@/contexts/AuthContext'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Button } from '@/components/ui/button'; +import { RefreshCw, User, Mic, MicOff, Crown, ArrowLeft, ServerCrash, UserX, Phone, Lock, LogIn } from 'lucide-react'; +import Link from 'next/link'; +import { Badge } from '@/components/ui/badge'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; + +export default function ConferenceDetails({ conferenceId }) { + const [participants, setParticipants] = useState([]); + const [conferenceInfo, setConferenceInfo] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { token, isLoggedIn } = useAuth(); + const [isConnecting, setIsConnecting] = useState(false); + const [currentUserExtension, setCurrentUserExtension] = useState(null); + + useEffect(() => { + if (isLoggedIn && token) { + const fetchUserDetails = async () => { + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/me`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + if (res.ok) { + const data = await res.json(); + setCurrentUserExtension(data.extensionId); + } + } catch (err) { + console.error("Failed to fetch user details", err); + } + }; + fetchUserDetails(); + } + }, [isLoggedIn, token]); + + const fetchDetails = useCallback(async () => { + setLoading(true); + setError(null); + try { + // Fetch both participants and conference room info + const [participantsRes, roomsRes] = await Promise.all([ + fetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences/${conferenceId}`), + fetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences`) + ]); + + if (!participantsRes.ok) { + const errorData = await participantsRes.json(); + if (participantsRes.status === 401) throw new Error("Unauthorized: Please log in to view conference details."); + throw new Error(errorData.error || `Failed to fetch details for conference ${conferenceId}`); + } + const participantsData = await participantsRes.json(); + setParticipants(Array.isArray(participantsData) ? participantsData : []); + + if (roomsRes.ok) { + const roomsData = await roomsRes.json(); + const currentRoom = roomsData.find(room => room.conferenceId === conferenceId); + setConferenceInfo(currentRoom); + } else { + // If fetching all rooms fails, we can assume we don't know the lock status + setConferenceInfo(null); + } + + } catch (err) { + console.error(`Failed to fetch details for ${conferenceId}`, err); + setError(err.message); + } finally { + setLoading(false); + } + }, [conferenceId]); + + useEffect(() => { + fetchDetails(); + const interval = setInterval(fetchDetails, 5000); // Refresh every 5 seconds + return () => clearInterval(interval); + }, [fetchDetails]); + + const handleConnectMe = async () => { + if (!token) return; + setIsConnecting(true); + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences/${conferenceId}/connectme`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` } + }); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.error || 'Failed to connect to conference'); + } + // Optionally show a success message + } catch (err) { + console.error("Failed to connect", err); + alert(err.message); + } finally { + setIsConnecting(false); + } + }; + + const isUserInConference = participants.some(p => p.callerIdNum === currentUserExtension); + + const ConnectButton = () => { + if (!isLoggedIn) { + return ( + + ); + } + + if (conferenceInfo?.locked) { + return ( + + ); + } + + if (isUserInConference) { + return ( + + ); + } + + return ( + + ); + }; + + return ( ++ Participants currently in the room. +
+There are no participants in this conference room.
++ Join a conversation or see who's currently in a conference. +
+When a conference starts, it will appear here.
+