Skip to content

Commit e13cac7

Browse files
Refactor UpcomingPage component and add CallList component
1 parent 9326ad5 commit e13cac7

File tree

6 files changed

+274
-8
lines changed

6 files changed

+274
-8
lines changed

app/(root)/(home)/upcoming/page.tsx

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import React from 'react'
1+
import CallList from '@/components/CallList';
22

3-
const Upcoming = () => {
3+
const UpcomingPage = () => {
44
return (
55
<section className="flex size-full flex-col gap-10 text-white">
6-
<h1 className="text-3xl font-bold"></h1>
7-
upcoming
6+
<h1 className="text-3xl font-bold">Upcoming Meeting</h1>
7+
8+
<CallList type="upcoming" />
89
</section>
9-
)
10-
}
10+
);
11+
};
1112

12-
export default Upcoming
13+
export default UpcomingPage;

app/constants/index.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,12 @@ export const sidebarLinks = [
2424
route: '/personal-room',
2525
imgUrl: '/icons/add-personal.svg'
2626
}
27-
]
27+
]
28+
29+
export const avatarImages = [
30+
'/images/avatar-1.jpeg',
31+
'/images/avatar-2.jpeg',
32+
'/images/avatar-3.png',
33+
'/images/avatar-4.png',
34+
'/images/avatar-5.png',
35+
];

components/CallList.tsx

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use client';
2+
3+
import { Call, CallRecording } from '@stream-io/video-react-sdk';
4+
5+
import Loader from './Loader';
6+
import { useGetCalls } from '@/hooks/useGetCalls';
7+
import MeetingCard from './MeetingCard';
8+
import { useEffect, useState } from 'react';
9+
import { useRouter } from 'next/navigation';
10+
11+
const CallList = ({ type }: { type: 'ended' | 'upcoming' | 'recordings' }) => {
12+
const router = useRouter();
13+
const { endedCalls, upcomingCalls, callRecordings, isLoading } =
14+
useGetCalls();
15+
const [recordings, setRecordings] = useState<CallRecording[]>([]);
16+
17+
const getCalls = () => {
18+
switch (type) {
19+
case 'ended':
20+
return endedCalls;
21+
case 'recordings':
22+
return recordings;
23+
case 'upcoming':
24+
return upcomingCalls;
25+
default:
26+
return [];
27+
}
28+
};
29+
30+
const getNoCallsMessage = () => {
31+
switch (type) {
32+
case 'ended':
33+
return 'No Previous Calls';
34+
case 'upcoming':
35+
return 'No Upcoming Calls';
36+
case 'recordings':
37+
return 'No Recordings';
38+
default:
39+
return '';
40+
}
41+
};
42+
43+
useEffect(() => {
44+
const fetchRecordings = async () => {
45+
const callData = await Promise.all(
46+
callRecordings?.map((meeting) => meeting.queryRecordings()) ?? [],
47+
);
48+
49+
const recordings = callData
50+
.filter((call) => call.recordings.length > 0)
51+
.flatMap((call) => call.recordings);
52+
53+
setRecordings(recordings);
54+
};
55+
56+
if (type === 'recordings') {
57+
fetchRecordings();
58+
}
59+
}, [type, callRecordings]);
60+
61+
if (isLoading) return <Loader />;
62+
63+
const calls = getCalls();
64+
const noCallsMessage = getNoCallsMessage();
65+
66+
return (
67+
<div className="grid grid-cols-1 gap-5 xl:grid-cols-2">
68+
{calls && calls.length > 0 ? (
69+
calls.map((meeting: Call | CallRecording) => (
70+
<MeetingCard
71+
key={(meeting as Call).id}
72+
icon={
73+
type === 'ended'
74+
? '/icons/previous.svg'
75+
: type === 'upcoming'
76+
? '/icons/upcoming.svg'
77+
: '/icons/recordings.svg'
78+
}
79+
title={
80+
(meeting as Call).state?.custom?.description ||
81+
(meeting as CallRecording).filename?.substring(0, 20) ||
82+
'No Description'
83+
}
84+
date={
85+
(meeting as Call).state?.startsAt?.toLocaleString() ||
86+
(meeting as CallRecording).start_time?.toLocaleString()
87+
}
88+
isPreviousMeeting={type === 'ended'}
89+
link={
90+
type === 'recordings'
91+
? (meeting as CallRecording).url
92+
: `${process.env.NEXT_PUBLIC_BASE_URL}/meeting/${(meeting as Call).id}`
93+
}
94+
buttonIcon1={type === 'recordings' ? '/icons/play.svg' : undefined}
95+
buttonText={type === 'recordings' ? 'Play' : 'Start'}
96+
handleClick={
97+
type === 'recordings'
98+
? () => router.push(`${(meeting as CallRecording).url}`)
99+
: () => router.push(`/meeting/${(meeting as Call).id}`)
100+
}
101+
/>
102+
))
103+
) : (
104+
<h1 className="text-2xl font-bold text-white">{noCallsMessage}</h1>
105+
)}
106+
</div>
107+
);
108+
};
109+
110+
export default CallList;

components/MeetingCard.tsx

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
5+
import { cn } from "@/lib/utils";
6+
import { Button } from "./ui/button";
7+
import { avatarImages } from "@/app/constants";
8+
import { useToast } from "./ui/toaster";
9+
10+
interface MeetingCardProps {
11+
title: string;
12+
date: string;
13+
icon: string;
14+
isPreviousMeeting?: boolean;
15+
buttonIcon1?: string;
16+
buttonText?: string;
17+
handleClick: () => void;
18+
link: string;
19+
}
20+
21+
const MeetingCard = ({
22+
icon,
23+
title,
24+
date,
25+
isPreviousMeeting,
26+
buttonIcon1,
27+
handleClick,
28+
link,
29+
buttonText,
30+
}: MeetingCardProps) => {
31+
const { toast } = useToast();
32+
33+
return (
34+
<section className="flex min-h-[258px] w-full flex-col justify-between rounded-[14px] bg-dark-1 px-5 py-8 xl:max-w-[568px]">
35+
<article className="flex flex-col gap-5">
36+
<Image src={icon} alt="upcoming" width={28} height={28} />
37+
<div className="flex justify-between">
38+
<div className="flex flex-col gap-2">
39+
<h1 className="text-2xl font-bold">{title}</h1>
40+
<p className="text-base font-normal">{date}</p>
41+
</div>
42+
</div>
43+
</article>
44+
<article className={cn("flex justify-center relative", {})}>
45+
<div className="relative flex w-full max-sm:hidden">
46+
{avatarImages.map((img, index) => (
47+
<Image
48+
key={index}
49+
src={img}
50+
alt="attendees"
51+
width={40}
52+
height={40}
53+
className={cn("rounded-full", { absolute: index > 0 })}
54+
style={{ top: 0, left: index * 28 }}
55+
/>
56+
))}
57+
<div className="flex-center absolute left-[136px] size-10 rounded-full border-[5px] border-dark-3 bg-dark-4">
58+
+5
59+
</div>
60+
</div>
61+
{!isPreviousMeeting && (
62+
<div className="flex gap-2">
63+
<Button onClick={handleClick} className="rounded bg-blue-1 px-6">
64+
{buttonIcon1 && (
65+
<Image src={buttonIcon1} alt="feature" width={20} height={20} />
66+
)}
67+
&nbsp; {buttonText}
68+
</Button>
69+
<Button
70+
onClick={() => {
71+
navigator.clipboard.writeText(link);
72+
toast({
73+
title: "Link Copied",
74+
});
75+
}}
76+
className="bg-dark-4 px-6"
77+
>
78+
<Image
79+
src="/icons/copy.svg"
80+
alt="feature"
81+
width={20}
82+
height={20}
83+
/>
84+
&nbsp; Copy Link
85+
</Button>
86+
</div>
87+
)}
88+
</article>
89+
</section>
90+
);
91+
};
92+
93+
export default MeetingCard;

components/ui/toaster.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ export function Toaster() {
3333
</ToastProvider>
3434
)
3535
}
36+
37+
export { useToast }

hooks/useGetCalls.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useEffect, useState } from 'react';
2+
import { useUser } from '@clerk/nextjs';
3+
import { Call, useStreamVideoClient } from '@stream-io/video-react-sdk';
4+
5+
export const useGetCalls = () => {
6+
const { user } = useUser();
7+
const client = useStreamVideoClient();
8+
const [calls, setCalls] = useState<Call[]>();
9+
const [isLoading, setIsLoading] = useState(false);
10+
11+
useEffect(() => {
12+
const loadCalls = async () => {
13+
if (!client || !user?.id) return;
14+
15+
setIsLoading(true);
16+
17+
try {
18+
// https://getstream.io/video/docs/react/guides/querying-calls/#filters
19+
const { calls } = await client.queryCalls({
20+
sort: [{ field: 'starts_at', direction: -1 }],
21+
filter_conditions: {
22+
starts_at: { $exists: true },
23+
$or: [
24+
{ created_by_user_id: user.id },
25+
{ members: { $in: [user.id] } },
26+
],
27+
},
28+
});
29+
30+
setCalls(calls);
31+
} catch (error) {
32+
console.error(error);
33+
} finally {
34+
setIsLoading(false);
35+
}
36+
};
37+
38+
loadCalls();
39+
}, [client, user?.id]);
40+
41+
const now = new Date();
42+
43+
const endedCalls = calls?.filter(({ state: { startsAt, endedAt } }: Call) => {
44+
return (startsAt && new Date(startsAt) < now) || !!endedAt
45+
})
46+
47+
const upcomingCalls = calls?.filter(({ state: { startsAt } }: Call) => {
48+
return startsAt && new Date(startsAt) > now
49+
})
50+
51+
return { endedCalls, upcomingCalls, callRecordings: calls, isLoading }
52+
};

0 commit comments

Comments
 (0)