From 2b96444dd7bbd2e2e35419fae51806ba142f4f00 Mon Sep 17 00:00:00 2001 From: LimJiaYan Date: Mon, 28 Oct 2024 19:46:57 +0800 Subject: [PATCH 1/6] Add question history page --- frontend/.env.local | 2 +- frontend/app/(authenticated)/layout.tsx | 12 +- .../profile/question-history/columns.tsx | 172 ++++++++++++++++++ .../profile/question-history/page.tsx | 139 ++++++++++++++ .../question-repo/data-table-toolbar.tsx | 9 +- .../question-repo/data-table.tsx | 4 +- 6 files changed, 331 insertions(+), 7 deletions(-) create mode 100644 frontend/app/(authenticated)/profile/question-history/columns.tsx create mode 100644 frontend/app/(authenticated)/profile/question-history/page.tsx diff --git a/frontend/.env.local b/frontend/.env.local index cb1f5a60aa..df6282427f 100644 --- a/frontend/.env.local +++ b/frontend/.env.local @@ -14,7 +14,7 @@ USER_API_BASE_URL=$PUBLIC_URL:$USER_API_PORT NEXT_PUBLIC_USER_API_AUTH_URL=$USER_API_BASE_URL/auth NEXT_PUBLIC_USER_API_USERS_URL=$USER_API_BASE_URL/users NEXT_PUBLIC_USER_API_EMAIL_URL=$USER_API_BASE_URL/email - +NEXT_PUBLIC_USER_API_HISTORY_URL=$USER_API_BASE_URL/history # Matching service MATCHING_API_PORT=3002 diff --git a/frontend/app/(authenticated)/layout.tsx b/frontend/app/(authenticated)/layout.tsx index 23c431611d..2e9da48ea7 100644 --- a/frontend/app/(authenticated)/layout.tsx +++ b/frontend/app/(authenticated)/layout.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { User, LogOut } from "lucide-react"; +import { User, FileText, LogOut } from "lucide-react"; import { usePathname } from "next/navigation"; export default function AuthenticatedLayout({ @@ -65,9 +65,15 @@ export default function AuthenticatedLayout({ - Username + My Account + + + Profile + + + Attempt History + - Profile { // e.preventDefault(); localStorage.removeItem("token"); diff --git a/frontend/app/(authenticated)/profile/question-history/columns.tsx b/frontend/app/(authenticated)/profile/question-history/columns.tsx new file mode 100644 index 0000000000..f6dca6a990 --- /dev/null +++ b/frontend/app/(authenticated)/profile/question-history/columns.tsx @@ -0,0 +1,172 @@ +"use client" + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; +import { ColumnDef } from "@tanstack/react-table" +import { AlignLeft, ArrowUpDown } from "lucide-react" + +export type QuestionHistory = { + id: number; + title: string; + complexity: string; + categories: string[]; + description: string; + attemptDate: Date; + attemptCount: number; + attemptTime: number; + }; + +export const columns : ColumnDef[]= [ + { + accessorKey: "id", + header: ({ column }) => { + return ( + + ) + }, + }, + { + accessorKey: "title", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => { + return ( + + +
+ + {row.getValue("title")} + +
+
+ +
+
+ + Description +
+
+

{row.original.description}

+
+
+
+
+ ) + }, + }, + { + header: ({ column }) => { + return ( + + ) + }, + accessorKey: "categories", + cell: ({ row }) => ( +
+ {row.original.categories.map((category, index) => ( + + {category} + + ))} +
+ ), + filterFn: (row, id, selectedCategories) => { + const rowCategories = row.getValue(id); + console.log(selectedCategories); + console.log(rowCategories); + return selectedCategories.every(category => rowCategories.includes(category)); + }, + }, + { + header: ({ column }) => { + return ( + + ) + }, + accessorKey: "complexity", + cell: ({ row }) => ( +
+ + {row.original.complexity} + +
+ ), + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + }, + { + header: ({ column }) => { + return ( + + ) + }, + accessorKey: "attemptCount", + cell: ({ row }) =>
{row.getValue("attemptCount")}
, + }, + { + header: ({ column }) => { + return ( + + ) + }, + accessorKey: "attemptTime", + cell: ({ row }) =>
{ Math.floor(row.getValue("attemptTime")/60)}
, + // Cell: ({ value }) => Math.floor(value / 60), // Convert time spent in seconds to minutes + }, + { + header: ({ column }) => { + return ( + + ) + }, + accessorKey: "attemptDate", + cell: ({ row }) => row.getValue("attemptDate").toLocaleString(), + }, + ]; \ No newline at end of file diff --git a/frontend/app/(authenticated)/profile/question-history/page.tsx b/frontend/app/(authenticated)/profile/question-history/page.tsx new file mode 100644 index 0000000000..ecfe6c1d84 --- /dev/null +++ b/frontend/app/(authenticated)/profile/question-history/page.tsx @@ -0,0 +1,139 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import { QuestionHistory, columns} from "./columns"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DataTable } from "../../question-repo/data-table"; +import { useRouter } from "next/navigation"; + +type receiveQuestion = { + attemptDate: string, + attemptCount: number, + attemptTime: number, + question: { + id: number; + title: string; + complexity: string; + category: string[]; + description: string; + link: string; + } +} + +export default function UserQuestionHistory() { + const router = useRouter(); + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + const userId = useRef(null); + + // authenticate user else redirect them to login page + useEffect(() => { + const authenticateUser = async () => { + try { + const token = localStorage.getItem("token"); + + if (!token) { + router.push("/auth/login"); // Redirect to login if no token + return; + } + + // Call the API to verify the token + const response = await fetch( + `${process.env.NEXT_PUBLIC_USER_API_AUTH_URL}/verify-token`, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response.ok) { + localStorage.removeItem("token"); // remove invalid token from browser + router.push("/auth/login"); // Redirect to login if not authenticated + return; + } + + const data = (await response.json()).data; + userId.current = data.id; + } catch (error) { + console.error("Error during authentication:", error); + router.push("/auth/login"); // Redirect to login in case of any error + } + }; + + authenticateUser(); + }, []); + + // Fetch questions history from backend API + useEffect(() => { + async function fetchHistory() { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_USER_API_HISTORY_URL}/${userId}` + ); + const data = await response.json(); + + // const data: receiveQuestion[] = [ + // { + // "attemptDate": "2024-10-12T12:34:56Z", + // "attemptCount": 2, + // "attemptTime": 600, // In seconds (e.g., 10 minutes) + // "question": { + // "id": 1, + // "title": "Two Sum", + // "complexity": "Easy", + // "category": ["Arrays", "Algorithms"], + // "description": "Find two numbers that add up to the target.", + // "link": "http://leetcode/" + // } + // }, + // { + // "attemptDate": "2024-10-10T15:10:45Z", + // "attemptCount": 1, + // "attemptTime": 180, // In seconds (e.g., 3 minutes) + // "question": { + // "id": 2, + // "title": "Longest Substring Without Repeating Characters", + // "complexity": "Medium", + // "category": ["Strings", "Algorithms"], + // "description": "Find the longest substring without repeating characters.", + // "link": "http://leetcode/" + // } + // }]; + + // Map backend data to match the frontend Question type + const mappedQuestions: QuestionHistory[] = data.map((q: receiveQuestion) => ({ + id: q.question.id, + title: q.question.title, + complexity: q.question.complexity, + categories: q.question.category.sort((a: string, b: string) => + a.localeCompare(b) + ), + description: q.question.description, + attemptDate: new Date(q.attemptDate), + attemptCount: q.attemptCount, + attemptTime: q.attemptTime, + }) + ); + + setHistory(mappedQuestions); + } catch (err) { + console.log("Error fetching questions from server:", err) + } finally { + setLoading(false); + } + } + + fetchHistory(); + }, []); + + // Success state: Render the list of attempted questions + return ( +
+
My Question History
+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/app/(authenticated)/question-repo/data-table-toolbar.tsx b/frontend/app/(authenticated)/question-repo/data-table-toolbar.tsx index 0bf4ec90cf..d8931b6d35 100644 --- a/frontend/app/(authenticated)/question-repo/data-table-toolbar.tsx +++ b/frontend/app/(authenticated)/question-repo/data-table-toolbar.tsx @@ -17,10 +17,11 @@ interface DataTableToolbarProps { table: Table data?: TData[] setData?: React.Dispatch> + isVisible: boolean } export function DataTableToolbar({ - table, data, setData + table, data, setData, isVisible }: DataTableToolbarProps) { const isFiltered = table.getState().columnFilters.length > 0 const isFilteredRowsSelected = table.getFilteredSelectedRowModel().rows.length > 0 @@ -97,7 +98,9 @@ export function DataTableToolbar({ )} - + + {isVisible && ( + <>
{isFilteredRowsSelected && ( @@ -133,6 +136,8 @@ export function DataTableToolbar({
+ + )} ) } \ No newline at end of file diff --git a/frontend/app/(authenticated)/question-repo/data-table.tsx b/frontend/app/(authenticated)/question-repo/data-table.tsx index ce9cbb208e..c5fb8bebc2 100644 --- a/frontend/app/(authenticated)/question-repo/data-table.tsx +++ b/frontend/app/(authenticated)/question-repo/data-table.tsx @@ -33,6 +33,7 @@ interface DataTableProps { data: TData[] setData?: React.Dispatch>; loading: boolean + isVisible?: boolean } export function DataTable({ @@ -40,6 +41,7 @@ export function DataTable({ data, setData, loading, + isVisible = true, }: DataTableProps) { const [rowSelection, setRowSelection] = useState({}) const [sorting, setSorting] = useState([]) @@ -106,7 +108,7 @@ export function DataTable({ return (
- +
From 6a859fbb339993f9a4d268b72b5fcc1ee5d3fc02 Mon Sep 17 00:00:00 2001 From: LimJiaYan Date: Mon, 28 Oct 2024 23:03:57 +0800 Subject: [PATCH 2/6] Update user profile page to show question attempt stats --- frontend/app/(authenticated)/profile/page.tsx | 35 +++++++++++++++++-- .../profile/question-history/page.tsx | 2 -- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/frontend/app/(authenticated)/profile/page.tsx b/frontend/app/(authenticated)/profile/page.tsx index ae5c513785..a2d43e7858 100644 --- a/frontend/app/(authenticated)/profile/page.tsx +++ b/frontend/app/(authenticated)/profile/page.tsx @@ -17,11 +17,17 @@ export default function Home() { username: "johndoe", email: "john@example.com", password: "abcdefgh", + totalAttempt: 0, + questionAttempt: 0, + totalQuestion: 20, }); const initialUserData = useRef({ username: "johndoe", email: "john@example.com", password: "abcdefgh", + totalAttempt: 0, + questionAttempt: 0, + totalQuestion: 20, }) const userId = useRef(null); @@ -52,16 +58,38 @@ export default function Home() { const data = (await response.json()).data; // placeholder for password *Backend wont expose password via any API call const password = ""; + + // Call the API to fetch user question history stats + const questionHistoryResponse = await fetch(`${process.env.NEXT_PUBLIC_USER_API_HISTORY_URL}/${data.id}/stats`, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!questionHistoryResponse.ok) { + localStorage.removeItem("token"); // remove invalid token from browser + router.push('/auth/login'); // Redirect to login if not authenticated + return; + } + + const stats = (await questionHistoryResponse.json()).data; setUserData({ username: data.username, email: data.email, password: password, + totalAttempt: stats.totalQuestionsAvailable, + questionAttempt: stats.questionsAttempted, + totalQuestion: stats.totalAttempts, }) initialUserData.current = { username: data.username, email: data.email, password: password, + totalAttempt: stats.totalQuestionsAvailable, + questionAttempt: stats.questionsAttempted, + totalQuestion: stats.totalAttempts, }; userId.current = data.id; } catch (error) { @@ -69,6 +97,7 @@ export default function Home() { router.push('/auth/login'); // Redirect to login in case of any error } }; + authenticateUser(); }, []); @@ -304,14 +333,14 @@ export default function Home() {
- 11 + {userData.questionAttempt} / - 20 + {userData.totalQuestion}
-

14

+

{userData.totalAttempt}

diff --git a/frontend/app/(authenticated)/profile/question-history/page.tsx b/frontend/app/(authenticated)/profile/question-history/page.tsx index ecfe6c1d84..f0aea2cc47 100644 --- a/frontend/app/(authenticated)/profile/question-history/page.tsx +++ b/frontend/app/(authenticated)/profile/question-history/page.tsx @@ -2,8 +2,6 @@ import React, { useEffect, useRef, useState } from "react"; import { QuestionHistory, columns} from "./columns"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Skeleton } from "@/components/ui/skeleton"; import { DataTable } from "../../question-repo/data-table"; import { useRouter } from "next/navigation"; From 591d75a08c04533a03da37aa8d5df377c0ba8d76 Mon Sep 17 00:00:00 2001 From: LimJiaYan Date: Thu, 31 Oct 2024 18:26:27 +0800 Subject: [PATCH 3/6] Debug for user question history page --- backend/user-service/routes/user-routes.js | 4 +- frontend/.env.local | 2 +- frontend/app/(authenticated)/profile/page.tsx | 29 +++++---- .../profile/question-history/page.tsx | 60 ++++++++----------- 4 files changed, 44 insertions(+), 51 deletions(-) diff --git a/backend/user-service/routes/user-routes.js b/backend/user-service/routes/user-routes.js index b537db754b..bb53aa2817 100644 --- a/backend/user-service/routes/user-routes.js +++ b/backend/user-service/routes/user-routes.js @@ -20,10 +20,10 @@ router.get("/", verifyAccessToken, verifyIsAdmin, getAllUsers); router.get("/check", checkUserExistByEmailorId); -router.get("/history/:userId", verifyAccessToken, getUserHistory); - router.get("/history/:userId/stats", verifyAccessToken, getUserStats); +router.get("/history/:userId", verifyAccessToken, getUserHistory); + router.patch("/:id/privilege", verifyAccessToken, verifyIsAdmin, updateUserPrivilege); router.post("/", createUser); diff --git a/frontend/.env.local b/frontend/.env.local index df6282427f..038962c744 100644 --- a/frontend/.env.local +++ b/frontend/.env.local @@ -14,7 +14,7 @@ USER_API_BASE_URL=$PUBLIC_URL:$USER_API_PORT NEXT_PUBLIC_USER_API_AUTH_URL=$USER_API_BASE_URL/auth NEXT_PUBLIC_USER_API_USERS_URL=$USER_API_BASE_URL/users NEXT_PUBLIC_USER_API_EMAIL_URL=$USER_API_BASE_URL/email -NEXT_PUBLIC_USER_API_HISTORY_URL=$USER_API_BASE_URL/history +NEXT_PUBLIC_USER_API_HISTORY_URL=$USER_API_BASE_URL/users/history # Matching service MATCHING_API_PORT=3002 diff --git a/frontend/app/(authenticated)/profile/page.tsx b/frontend/app/(authenticated)/profile/page.tsx index 08ceed5a9b..ef83576daf 100644 --- a/frontend/app/(authenticated)/profile/page.tsx +++ b/frontend/app/(authenticated)/profile/page.tsx @@ -12,6 +12,7 @@ import React, { ChangeEvent, useEffect, useRef, useState } from "react"; export default function Home() { const router = useRouter(); + const [error, setError] = useState(false); const [feedback, setFeedback] = useState({ message: '', type: '' }); const [isEditing, setIsEditing] = useState(false); const [userData, setUserData] = useState({ @@ -57,6 +58,10 @@ export default function Home() { } const data = (await response.json()).data; + if (!getCookie('userId')) { + userId.current = data.id; + setCookie('userId', data.id, { 'max-age': '86400', 'path': '/', 'SameSite': 'Strict' }); + } // placeholder for password *Backend wont expose password via any API call const password = ""; @@ -69,30 +74,27 @@ export default function Home() { }); if (!questionHistoryResponse.ok) { - localStorage.removeItem("token"); // remove invalid token from browser - router.push('/auth/login'); // Redirect to login if not authenticated - return; + setError(true); } - const stats = (await questionHistoryResponse.json()).data; - + const stats = await questionHistoryResponse.json(); + console.log("stats", stats) setUserData({ username: data.username, email: data.email, password: password, - totalAttempt: stats.totalQuestionsAvailable, - questionAttempt: stats.questionsAttempted, - totalQuestion: stats.totalAttempts, + totalAttempt: stats?.totalQuestionsAvailable, + questionAttempt: stats?.questionsAttempted, + totalQuestion: stats?.totalAttempts, }) initialUserData.current = { username: data.username, email: data.email, password: password, - totalAttempt: stats.totalQuestionsAvailable, - questionAttempt: stats.questionsAttempted, - totalQuestion: stats.totalAttempts, + totalAttempt: stats?.totalQuestionsAvailable, + questionAttempt: stats?.questionsAttempted, + totalQuestion: stats?.totalAttempts, }; - userId.current = data.id; } catch (error) { console.error('Error during authentication:', error); router.push('/auth/login'); // Redirect to login in case of any error @@ -331,6 +333,8 @@ export default function Home() { onChange={handleInputChange} /> + + { !error &&
@@ -345,6 +349,7 @@ export default function Home() {

{userData.totalAttempt}

+ } diff --git a/frontend/app/(authenticated)/profile/question-history/page.tsx b/frontend/app/(authenticated)/profile/question-history/page.tsx index f0aea2cc47..00029a61a1 100644 --- a/frontend/app/(authenticated)/profile/question-history/page.tsx +++ b/frontend/app/(authenticated)/profile/question-history/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { deleteCookie, getCookie, setCookie } from "@/app/utils/cookie-manager"; import React, { useEffect, useRef, useState } from "react"; import { QuestionHistory, columns} from "./columns"; import { DataTable } from "../../question-repo/data-table"; @@ -25,54 +26,41 @@ export default function UserQuestionHistory() { const [loading, setLoading] = useState(true); const userId = useRef(null); - // authenticate user else redirect them to login page + // Fetch questions history from backend API useEffect(() => { - const authenticateUser = async () => { + async function fetchHistory() { try { - const token = localStorage.getItem("token"); + userId.current = getCookie('userId'); + + if (!userId.current) { + // Call the API to get user id + const response = await fetch(`${process.env.NEXT_PUBLIC_USER_API_AUTH_URL}/verify-token`, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getCookie('token')}`, + }, + }); - if (!token) { - router.push("/auth/login"); // Redirect to login if no token - return; + const data = (await response.json()).data; + setCookie('userId', data.id, { 'max-age': '86400', 'path': '/', 'SameSite': 'Strict' }); } - // Call the API to verify the token + console.log("In question history page: call api to fetch user question history") const response = await fetch( - `${process.env.NEXT_PUBLIC_USER_API_AUTH_URL}/verify-token`, - { + `${process.env.NEXT_PUBLIC_USER_API_HISTORY_URL}/${getCookie('userId')}`, { headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getCookie('token')}`, }, } ); - + + const data = await response.json(); if (!response.ok) { - localStorage.removeItem("token"); // remove invalid token from browser - router.push("/auth/login"); // Redirect to login if not authenticated - return; + console.error("Error:", data.message); + return; } - - const data = (await response.json()).data; - userId.current = data.id; - } catch (error) { - console.error("Error during authentication:", error); - router.push("/auth/login"); // Redirect to login in case of any error - } - }; - - authenticateUser(); - }, []); - - // Fetch questions history from backend API - useEffect(() => { - async function fetchHistory() { - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_USER_API_HISTORY_URL}/${userId}` - ); - const data = await response.json(); - + // const data: receiveQuestion[] = [ // { // "attemptDate": "2024-10-12T12:34:56Z", From cd10c800a58c1eedde125eecad1425aa0cfe6a6d Mon Sep 17 00:00:00 2001 From: LimJiaYan Date: Thu, 31 Oct 2024 18:51:14 +0800 Subject: [PATCH 4/6] Add API call to insert question attempt into backend --- frontend/app/(authenticated)/session/[id]/page.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/app/(authenticated)/session/[id]/page.tsx b/frontend/app/(authenticated)/session/[id]/page.tsx index 1b92d5d367..015bff636b 100644 --- a/frontend/app/(authenticated)/session/[id]/page.tsx +++ b/frontend/app/(authenticated)/session/[id]/page.tsx @@ -12,6 +12,7 @@ import { Toaster } from '@/components/ui/sonner'; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { useRouter } from 'next/navigation'; import SessionLoading from '../loading'; +import { getCookie } from '@/app/utils/cookie-manager'; const DynamicCodeEditor = dynamic(() => import('../code-editor/code-editor'), { ssr: false }); const DynamicTextEditor = dynamic(() => import('../text-editor'), { ssr: false }); @@ -47,6 +48,19 @@ export default function Session({ params }: { params: { id: string } }) { }; async function endSession() { + // Call the API to update user question history + const questionHistoryResponse = await fetch(`${process.env.NEXT_PUBLIC_USER_API_HISTORY_URL}/${getCookie('userId')}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getCookie('token')}`, + }, + body: JSON.stringify({ + questionId: "1", // TODO: one question id that user has attempted + timeSpent: 120, // TODO: time spent in second + }), + }); + router.push('/questions'); } From 6f5879a7591fed82102765219123bb0e1fbb3628 Mon Sep 17 00:00:00 2001 From: LimJiaYan Date: Thu, 31 Oct 2024 23:01:09 +0800 Subject: [PATCH 5/6] Handle close tab --- .../app/(authenticated)/session/[id]/page.tsx | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/frontend/app/(authenticated)/session/[id]/page.tsx b/frontend/app/(authenticated)/session/[id]/page.tsx index 015bff636b..72a525f994 100644 --- a/frontend/app/(authenticated)/session/[id]/page.tsx +++ b/frontend/app/(authenticated)/session/[id]/page.tsx @@ -21,9 +21,23 @@ export default function Session({ params }: { params: { id: string } }) { const router = useRouter(); const [isClient, setIsClient] = useState(false); const [isMicEnabled, setIsMicEnabled] = useState(false); + const [isSessionEnded, setIsSessionEnded] = useState(false); // Flag to track if API call has been made useEffect(() => { setIsClient(true); + + // Add the event listener for the beforeunload event + const handleBeforeUnload = (event) => { + callUserHistoryAPI(); + event.preventDefault(); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + // Cleanup function to remove the event listener on component unmount + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; }, []); if (!isClient) { @@ -47,20 +61,31 @@ export default function Session({ params }: { params: { id: string } }) { } }; - async function endSession() { - // Call the API to update user question history - const questionHistoryResponse = await fetch(`${process.env.NEXT_PUBLIC_USER_API_HISTORY_URL}/${getCookie('userId')}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${getCookie('token')}`, - }, - body: JSON.stringify({ - questionId: "1", // TODO: one question id that user has attempted - timeSpent: 120, // TODO: time spent in second - }), - }); + // Update user question history before the page being unloaded + const callUserHistoryAPI = async () => { + if (isSessionEnded) return; + try { + console.log('In session page: Call api to udate user question history'); + await fetch(`${process.env.NEXT_PUBLIC_USER_API_HISTORY_URL}/${getCookie('userId')}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getCookie('token')}`, + }, + body: JSON.stringify({ + userId: getCookie('userId'), + questionId: "1", // TODO: one question id that user has attempted + timeSpent: 120, // TODO: time spent in second + }), + }); + setIsSessionEnded(true); // Set flag to prevent multiple calls + } catch (error) { + console.error('Failed to update question history:', error); + } + }; + async function endSession() { + await callUserHistoryAPI(); router.push('/questions'); } From 6b1941f1ac3c9b490a8c659e0137f3cff090e813 Mon Sep 17 00:00:00 2001 From: LimJiaYan Date: Fri, 1 Nov 2024 00:40:37 +0800 Subject: [PATCH 6/6] add timer for collab page --- backend/matching-service/Dockerfile.match | 2 +- .../app/(authenticated)/session/[id]/page.tsx | 44 ++++++++++++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/backend/matching-service/Dockerfile.match b/backend/matching-service/Dockerfile.match index 046e9bfaa8..4dbc389f28 100644 --- a/backend/matching-service/Dockerfile.match +++ b/backend/matching-service/Dockerfile.match @@ -1,4 +1,4 @@ -FROM node:14 +FROM node:16 WORKDIR /usr/src/app diff --git a/frontend/app/(authenticated)/session/[id]/page.tsx b/frontend/app/(authenticated)/session/[id]/page.tsx index 72a525f994..f2d306aad1 100644 --- a/frontend/app/(authenticated)/session/[id]/page.tsx +++ b/frontend/app/(authenticated)/session/[id]/page.tsx @@ -21,12 +21,27 @@ export default function Session({ params }: { params: { id: string } }) { const router = useRouter(); const [isClient, setIsClient] = useState(false); const [isMicEnabled, setIsMicEnabled] = useState(false); - const [isSessionEnded, setIsSessionEnded] = useState(false); // Flag to track if API call has been made + const [isRequestSent, setIsRequestSent] = useState(false); // Flag to track if API call has been made + const [isEndingSession, setIsEndingSession] = useState(false); + const [controller, setController] = useState(null); + const [timeElapsed, setTimeElapsed] = useState(0); + + useEffect(() => { + const timerInterval = setInterval(() => { + setTimeElapsed((prevTime) => prevTime + 1); + }, 1000); + + return () => clearInterval(timerInterval); + }, []); + + const minutes = Math.floor(timeElapsed / 60); + const seconds = timeElapsed % 60; useEffect(() => { setIsClient(true); // Add the event listener for the beforeunload event + // not always work, depend on browser const handleBeforeUnload = (event) => { callUserHistoryAPI(); event.preventDefault(); @@ -63,7 +78,12 @@ export default function Session({ params }: { params: { id: string } }) { // Update user question history before the page being unloaded const callUserHistoryAPI = async () => { - if (isSessionEnded) return; + if (isRequestSent) return; + + const abortController = new AbortController(); + setController(abortController); + setIsEndingSession(true); + try { console.log('In session page: Call api to udate user question history'); await fetch(`${process.env.NEXT_PUBLIC_USER_API_HISTORY_URL}/${getCookie('userId')}`, { @@ -75,12 +95,16 @@ export default function Session({ params }: { params: { id: string } }) { body: JSON.stringify({ userId: getCookie('userId'), questionId: "1", // TODO: one question id that user has attempted - timeSpent: 120, // TODO: time spent in second + timeSpent: timeElapsed, }), + signal: abortController.signal, }); - setIsSessionEnded(true); // Set flag to prevent multiple calls + setIsRequestSent(true); } catch (error) { console.error('Failed to update question history:', error); + } finally { + setIsEndingSession(false); + setController(null); } }; @@ -89,13 +113,20 @@ export default function Session({ params }: { params: { id: string } }) { router.push('/questions'); } + function handleCancel() { + if (controller) { + controller.abort(); // Cancel the API call + setIsEndingSession(false); + } + } + return (
Session {params.id} -
3:35
+
{minutes}:{seconds}
with username
@@ -130,7 +161,7 @@ export default function Session({ params }: { params: { id: string } }) { @@ -139,6 +170,7 @@ export default function Session({ params }: { params: { id: string } }) { type="submit" className="rounded-lg bg-brand-700 hover:bg-brand-600" onClick={endSession} + disabled={isEndingSession} > End session