Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoom link #187

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
48 changes: 48 additions & 0 deletions src/app/api/auth/callback/route.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use NextJS server actions instead? There's a lot of boiler plate with using route handlers.
https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextResponse } from "next/server";

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!process.env.ZOOM_CLIENT_SECRET) throw new Error("Missing environment variable ZOOM_CLIENT_SECRET in .env")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!process.env.ZOOM_REDIRECT_URI) throw new Error("Missing environment variable ZOOM_REDIRECT_URI in .env")

export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const code = searchParams.get("code");

if (!code) {
return NextResponse.json(
{ error: "No authorization code" },
{ status: 400 },
);
}

try {
const response = await fetch("https://zoom.us/oauth/token", {
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from(
`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`,
).toString("base64")}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: process.env.ZOOM_REDIRECT_URI!,
}).toString(),
});

const data = await response.json();

if (!data.access_token) {
return NextResponse.json(
{
error: "Failed to exchange authorization code for access token",
details: data,
},
{ status: 400 },
);
}

return NextResponse.redirect(
new URL(`/admin/schedule?zoom_token=${data.access_token}`, req.url),
);
} catch (error) {
return NextResponse.json({ error: "???" }, { status: 500 });
}
}
7 changes: 7 additions & 0 deletions src/app/api/auth/route.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw errors if missing envars

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextResponse } from "next/server";

export async function GET() {
const zoomAuthUrl = `https://zoom.us/oauth/authorize?response_type=code&client_id=${process.env.ZOOM_CLIENT_ID}&redirect_uri=${process.env.ZOOM_REDIRECT_URI}`;

return NextResponse.json({ url: zoomAuthUrl });
}
57 changes: 57 additions & 0 deletions src/app/api/create-meeting/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { NextResponse } from "next/server";

export async function POST(req: Request) {
const { zoom_token, startDateAndTime, presentationDuration } =
await req.json();

if (!zoom_token) {
return NextResponse.json(
{ error: "Error: Zoom token is missing." },
{ status: 401 },
);
}

try {
const response = await fetch("https://api.zoom.us/v2/users/me/meetings", {
method: "POST",
headers: {
Authorization: `Bearer ${zoom_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
topic: "Judging Session",
type: 2,
start_time: startDateAndTime,
duration: presentationDuration,
timezone: "UTC",
settings: {
waiting_room: true,
breakout_room: {
enable: true,
},
join_before_host: false,
mute_upon_entry: true,
approval_type: 0,
auto_recording: "none",
},
}),
});

const data = await response.json();

if (data.code) {
return NextResponse.json(
{ error: data.message || "Failed to create meeting" },
{ status: 400 },
);
}

return NextResponse.json({ join_url: data.join_url, meeting_id: data.id });
} catch (err) {
console.error("Error creating meeting:", err);
return NextResponse.json(
{ error: "Failed to create Zoom meeting." },
{ status: 500 },
);
}
}
104 changes: 57 additions & 47 deletions src/components/Dashboard/JudgingInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { generateClient } from "aws-amplify/api";
import Image from "next/image";
import { useEffect, useState } from "react";

import { type Schema } from "@/amplify/data/resource";
import JudgeIcon from "@/images/dashboard/JudgeIcon.png";
Expand All @@ -14,14 +15,13 @@ const client = generateClient<Schema>();

export default function JudgingInfo() {
const userId = useUser().currentUser.username as string;
const [showZoomLink, setShowZoomLink] = useState(false);

const { data: userData } = useQuery({
initialDataUpdatedAt: 0,
queryKey: ["User", userId],
queryFn: async () => {
const { data, errors } = await client.models.User.get({
id: userId,
});

const { data, errors } = await client.models.User.get({ id: userId });
if (errors) {
throw new Error("Error fetching user data");
}
Expand All @@ -30,78 +30,72 @@ export default function JudgingInfo() {
enabled: !!userId,
});

//Fetch team data using the teamId from userData

const teamId = userData?.teamId;
const { data: teamData, isFetching: isFetchingTeamName } = useQuery({
initialDataUpdatedAt: 0,
queryKey: ["Team", teamId],
queryFn: async () => {
if (!teamId) {
throw new Error("Team ID is undefined");
}
const { data, errors } = await client.models.Team.get({
id: teamId,
});

if (errors) {
throw new Error("Error fetching team data");
}
if (!teamId) throw new Error("Team ID is undefined");
const { data, errors } = await client.models.Team.get({ id: teamId });
if (errors) throw new Error("Error fetching team data");
return data;
},
enabled: !!teamId,
});

const teamName = teamData?.name || "Team";

//Fetch team data
const { data: teamRoomData, isFetching: isFetchingTeamRoom } = useQuery({
initialDataUpdatedAt: 0,
queryKey: ["TeamRoom", teamId],
queryFn: async () => {
if (!teamId) {
throw new Error("Team ID is undefined");
}
if (!teamId) throw new Error("Team ID is undefined");
const { data, errors } = await client.models.TeamRoom.list({
filter: {
teamId: { eq: teamId },
},
filter: { teamId: { eq: teamId } },
});

if (errors) {
throw new Error("Error fetching team room data");
}
if (errors) throw new Error("Error fetching team room data");
return data;
},
enabled: !!teamId,
});

const timeSlot = teamRoomData?.[0]?.time
? new Date(teamRoomData[0].time).toLocaleString()
: "Room not assigned";
? new Date(teamRoomData[0].time)
: null;

const zoomLink = teamRoomData?.[0]?.zoomLink || null;

useEffect(() => {
if (timeSlot) {
const checkTime = () => {
const now = new Date();
const fiveMinutesBefore = new Date(timeSlot);
fiveMinutesBefore.setMinutes(timeSlot.getMinutes() - 5);

setShowZoomLink(now >= fiveMinutesBefore);
};

checkTime();
const interval = setInterval(checkTime, 60000);

return () => clearInterval(interval);
}
}, [timeSlot]);

Comment on lines +68 to 84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are using dervied state to get the zoomLink, so why not use derived state to also compute setShowZoomLink? This way you subtract a useeffect and usestate hook improving performance

You can do this because of how tanstack query caches the teamroomdataand will update it based on its query key if anything changes

//Fetch room Id from Team Room
const roomId = teamRoomData?.[0]?.roomId;
const { data: judgeRoomData, isFetching: isFetchingJudgeData } = useQuery({
queryKey: ["Room", roomId],
queryFn: async () => {
if (!roomId) {
throw new Error("Room ID is undefined");
}
if (!roomId) throw new Error("Room ID is undefined");
const { data, errors } = await client.models.User.list({
filter: {
JUDGE_roomId: { eq: roomId },
},
filter: { JUDGE_roomId: { eq: roomId } },
});

if (errors) {
throw new Error("Error fetching team room data");
}
if (errors) throw new Error("Error fetching team room data");
return data;
},
enabled: !!roomId,
});

//Fetch judges in the same room
const judgeNames =
judgeRoomData
?.map((judge) => `${judge.firstName} ${judge.lastName}`)
Expand All @@ -115,33 +109,49 @@ export default function JudgingInfo() {
src={JudgeIcon}
alt={"Judging Icon"}
/>
<div className="text-start font-medium ">
<div className="text-start font-medium">
{isFetchingTeamName ? "Loading..." : <div>{`${teamName}'s `}</div>}
<div className="">Judging Information</div>
</div>
</div>

<div className="flex flex-col gap-2 p-4 text-start">
<div className="font-medium">Time Slot: </div>

{isFetchingTeamRoom ? (
"Loading..."
) : (
<div className=" text-3xl italic text-neutral-800 xl:text-5xl">
{timeSlot}
<div className="text-3xl italic text-neutral-800 xl:text-5xl">
{timeSlot ? timeSlot.toLocaleString() : "Room not assigned"}
</div>
)}
</div>

<div className="flex flex-col gap-2 p-4 text-start">
<div className="font-medium">Judges: </div>

{isFetchingJudgeData ? (
"Loading..."
) : (
<div className=" text-3xl italic text-neutral-800 xl:text-5xl">
<div className="text-3xl italic text-neutral-800 xl:text-5xl">
{judgeNames}
</div>
)}
</div>

<div className="flex flex-col gap-2 p-4 text-start">
<div className="font-medium">Zoom Link: </div>
{showZoomLink && zoomLink ? (
<a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use a tag instead?

href={zoomLink}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 underline"
>
Join Zoom Meeting
</a>
) : (
<div className="text-gray-500">Zoom link coming soon</div>
)}
</div>
</Card>
);
}
26 changes: 25 additions & 1 deletion src/components/admin/Judging/JudgingSchedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,33 @@ export default function JudgingSchedule() {
zoomLink: teamRoom.zoomLink,
}));

// Update all team rooms with the same zoom link (can change this to different zoom links later on)
const { mutate: updateTeamRoomsWithZoomLink } = useMutation({
mutationFn: async (zoomLink: string) => {
const { data, errors } = await client.models.TeamRoom.list();
if (errors) {
throw errors;
}

const updatePromises = data.map((teamRoom) =>
client.models.TeamRoom.update({
id: teamRoom.id,
zoomLink,
}),
);

await Promise.all(updatePromises);

queryClient.invalidateQueries({ queryKey: ["TeamRoom"] });
},
});

return (
<>
<RoomAssigner judgingScheduleMutation={mutate} />
<RoomAssigner
judgingScheduleMutation={mutate}
updateTeamRoomsWithZoomLink={updateTeamRoomsWithZoomLink}
/>
<div className="flex justify-center">
<div className="m-4 w-full max-w-[1500px] rounded-md border border-awesomer-purple bg-light-grey p-4 text-lg text-black">
{judgeRooms && judgingEvents ? (
Expand Down
Loading