Skip to content

Commit 3d30a01

Browse files
authored
Merge pull request #76 from CS3219-AY2425S1/feat/fe-for-matching
Feat/fe for matching
2 parents 9a4a8ec + da83053 commit 3d30a01

File tree

9 files changed

+406
-261
lines changed

9 files changed

+406
-261
lines changed

peerprep-fe/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"zustand": "5.0.0-rc.2"
3838
},
3939
"devDependencies": {
40+
"@types/amqplib": "^0.10.5",
4041
"@types/node": "^20",
4142
"@types/react": "^18",
4243
"@types/react-dom": "^18",

peerprep-fe/pnpm-lock.yaml

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

peerprep-fe/src/app/(main)/components/filter/FilterSelect.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
SelectContent,
44
SelectItem,
55
SelectTrigger,
6+
SelectValue,
67
} from '@/components/ui/select';
78
import { FilterSelectProps } from '@/types/types';
89

@@ -12,15 +13,20 @@ export function FilterSelect({
1213
onChange,
1314
isMulti,
1415
value,
15-
}: FilterSelectProps) {
16+
showSelectedValue = false, // New prop to control the display behavior
17+
}: FilterSelectProps & { showSelectedValue?: boolean }) {
1618
return (
1719
<Select
1820
onValueChange={onChange}
1921
{...(isMulti ? { multiple: true } : {})}
2022
value={isMulti ? undefined : (value as string)}
2123
>
2224
<SelectTrigger className="w-[120px] border-gray-700 bg-gray-800">
23-
{placeholder}
25+
{showSelectedValue ? (
26+
<SelectValue placeholder={placeholder} />
27+
) : (
28+
placeholder
29+
)}
2430
</SelectTrigger>
2531
<SelectContent>
2632
{options.map((option) => (

peerprep-fe/src/app/(main)/match/page.tsx

Lines changed: 116 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,38 @@ import { useState, useEffect } from 'react';
44
import { useRouter } from 'next/navigation';
55
import { User, Code } from 'lucide-react';
66
import { consumeMessageFromQueue } from '@/lib/rabbitmq';
7+
import { Button } from '@/components/ui/button';
8+
import { useAuthStore } from '@/state/useAuthStore';
79

810
export default function LoadingPage() {
911
const [elapsedTime, setElapsedTime] = useState(0);
1012
const [usersWaiting, setUsersWaiting] = useState(4);
13+
const [matchStatus, setMatchStatus] = useState('searching');
1114
const router = useRouter();
15+
const { user } = useAuthStore();
1216

1317
const startConsumingMessages = async () => {
1418
try {
15-
await consumeMessageFromQueue().then((message) => {
16-
// This function is called when a message is consumed
17-
if (message.status == 'matched') {
18-
console.log('Match found, your partner is');
19-
router.push('/');
19+
await consumeMessageFromQueue(user?.id!).then((message) => {
20+
if (message.status === 'matched') {
21+
console.log('Match found, your partner is', message.partner);
22+
setMatchStatus('matched');
23+
24+
// setTimeout(() => {
25+
// router.push(`/collaboration`);
26+
// }, 2000);
2027
} else {
2128
console.log('Match failed');
22-
router.push('/');
29+
setMatchStatus('failed');
30+
31+
// setTimeout(() => {
32+
// router.push(`/`);
33+
// }, 4500);
2334
}
2435
});
2536
} catch (error) {
2637
console.error('Error consuming message:', error);
38+
setMatchStatus('error');
2739
}
2840
};
2941

@@ -37,12 +49,17 @@ export default function LoadingPage() {
3749
}, []);
3850

3951
useEffect(() => {
40-
if (elapsedTime >= 60) {
41-
// Execute your action here
42-
console.log('Elapsed time reached 60 seconds. Going back to main page');
43-
router.push('/');
52+
if (elapsedTime >= 60 && matchStatus === 'searching') {
53+
console.log('Elapsed time reached 60 seconds. Match timed out.');
54+
setMatchStatus('timeout');
4455
}
45-
}, [elapsedTime]);
56+
}, [elapsedTime, matchStatus]);
57+
58+
const handleCancel = () => {
59+
// Implement logic to cancel the matching process
60+
console.log('Matching cancelled');
61+
router.push('/');
62+
};
4663

4764
return (
4865
<div className="flex min-h-screen flex-col bg-[#1a1f2e] text-gray-300">
@@ -54,30 +71,94 @@ export default function LoadingPage() {
5471
<User className="h-6 w-6" />
5572
</header>
5673
<main className="flex flex-grow flex-col items-center justify-center space-y-6 px-4">
57-
<div className="h-16 w-16 animate-spin rounded-full border-b-4 border-t-4 border-purple-500"></div>
58-
<h1 className="text-2xl font-bold text-white">Finding a match</h1>
59-
<p className="max-w-md text-center text-sm">
60-
We&apos;re pairing you with another coder. This may take a few
61-
moments.
62-
</p>
63-
<div className="w-full max-w-md space-y-2">
64-
<div className="h-1 overflow-hidden rounded-full bg-gray-700">
65-
<div
66-
className="h-full rounded-full bg-purple-500"
67-
style={{ width: `${((elapsedTime % 60) / 60) * 100}%` }}
68-
></div>
69-
</div>
70-
<div className="text-center text-sm">
71-
Time elapsed: {elapsedTime} seconds
72-
</div>
73-
</div>
74-
<div className="flex items-center space-x-2 text-sm">
75-
<User className="h-4 w-4" />
76-
<span>{usersWaiting} users waiting</span>
77-
</div>
78-
<button className="w-full max-w-md rounded-md bg-purple-600 px-4 py-2 text-white transition-colors hover:bg-purple-700">
79-
Cancel Matching
80-
</button>
74+
{matchStatus === 'searching' && (
75+
<>
76+
<div className="h-16 w-16 animate-spin rounded-full border-b-4 border-t-4 border-purple-500"></div>
77+
<h1 className="text-2xl font-bold text-white">Finding a match</h1>
78+
<p className="max-w-md text-center text-sm">
79+
We&apos;re pairing you with another coder. This may take a few
80+
moments.
81+
</p>
82+
<div className="w-full max-w-md space-y-2">
83+
<div className="h-1 overflow-hidden rounded-full bg-gray-700">
84+
<div
85+
className="h-full rounded-full bg-purple-500"
86+
style={{ width: `${((elapsedTime % 60) / 60) * 100}%` }}
87+
></div>
88+
</div>
89+
<div className="text-center text-sm">
90+
Time elapsed: {elapsedTime} seconds
91+
</div>
92+
</div>
93+
<div className="flex items-center space-x-2 text-sm">
94+
<User className="h-4 w-4" />
95+
<span>{usersWaiting} users waiting</span>
96+
</div>
97+
<Button
98+
onClick={handleCancel}
99+
className="w-full max-w-md bg-purple-600 hover:bg-purple-700"
100+
>
101+
Cancel Matching
102+
</Button>
103+
</>
104+
)}
105+
{matchStatus === 'matched' && (
106+
<>
107+
<div className="h-16 w-16 animate-bounce text-4xl">🎉</div>
108+
<h1 className="text-2xl font-bold text-white">Match Found!</h1>
109+
<p className="max-w-md text-center text-sm">
110+
Great news! We&apos;ve found a coding partner for you. Redirecting
111+
to your collaboration room...
112+
</p>
113+
</>
114+
)}
115+
{matchStatus === 'failed' && (
116+
<>
117+
<div className="h-16 w-16 text-4xl">😕</div>
118+
<h1 className="text-2xl font-bold text-white">Match Failed</h1>
119+
<p className="max-w-md text-center text-sm">
120+
We couldn&apos;t find a suitable match at this time. Please try
121+
again later.
122+
</p>
123+
<Button
124+
onClick={() => router.push('/')}
125+
className="w-full max-w-md bg-purple-600 hover:bg-purple-700"
126+
>
127+
Return to Home
128+
</Button>
129+
</>
130+
)}
131+
{matchStatus === 'timeout' && (
132+
<>
133+
<div className="h-16 w-16 text-4xl"></div>
134+
<h1 className="text-2xl font-bold text-white">Match Timed Out</h1>
135+
<p className="max-w-md text-center text-sm">
136+
We couldn&apos;t find a match within the time limit. Please try
137+
again.
138+
</p>
139+
<Button
140+
onClick={() => router.push('/')}
141+
className="w-full max-w-md bg-purple-600 hover:bg-purple-700"
142+
>
143+
Return to Home
144+
</Button>
145+
</>
146+
)}
147+
{matchStatus === 'error' && (
148+
<>
149+
<div className="h-16 w-16 text-4xl"></div>
150+
<h1 className="text-2xl font-bold text-white">Error Occurred</h1>
151+
<p className="max-w-md text-center text-sm">
152+
An error occurred while finding a match. Please try again later.
153+
</p>
154+
<Button
155+
onClick={() => router.push('/')}
156+
className="w-full max-w-md bg-purple-600 hover:bg-purple-700"
157+
>
158+
Return to Home
159+
</Button>
160+
</>
161+
)}
81162
<p className="mt-4 text-sm text-gray-500">
82163
Tip: While you wait, why not review some coding concepts?
83164
</p>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
3+
type Props = {};
4+
5+
const page = (props: Props) => {
6+
return <div>This is the collaboration page</div>;
7+
};
8+
9+
export default page;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogHeader,
9+
DialogTitle,
10+
DialogTrigger,
11+
} from '@/components/ui/dialog';
12+
import { Button } from '@/components/ui/button';
13+
import { FilterSelect } from '@/app/(main)/components/filter/FilterSelect';
14+
import { TopicsPopover } from '@/app/(main)/components/filter/TopicsPopover';
15+
import { sendMessageToQueue } from '@/lib/rabbitmq';
16+
import { axiosAuthClient } from '@/network/axiosClient';
17+
import { DIFFICULTY_OPTIONS } from '@/lib/constants';
18+
19+
export function PreMatch() {
20+
const [open, setOpen] = useState(false);
21+
const [difficulty, setDifficulty] = useState('');
22+
const [selectedTopics, setSelectedTopics] = useState<string[]>([]);
23+
const router = useRouter();
24+
25+
const handleConfirm = async () => {
26+
try {
27+
const profileDetails = await getProfileDetails();
28+
const message = {
29+
_id: profileDetails.id,
30+
name: profileDetails.username,
31+
topic: selectedTopics[0] || '', // TODO: change to list, but current backend only accepts 1
32+
// topic: selectedTopics.join(','),
33+
difficulty: difficulty,
34+
};
35+
await sendMessageToQueue(message);
36+
setOpen(false);
37+
router.push('/match');
38+
} catch (err) {
39+
console.error('Error in handleConfirm:', err);
40+
}
41+
};
42+
43+
const getProfileDetails = async () => {
44+
const result = await axiosAuthClient.get('/auth/verify-token');
45+
return result.data.data;
46+
};
47+
48+
return (
49+
<Dialog open={open} onOpenChange={setOpen}>
50+
<DialogTrigger asChild>
51+
<Button variant="ghost" className="text-gray-300 hover:text-white">
52+
Match
53+
</Button>
54+
</DialogTrigger>
55+
<DialogContent className="sm:max-w-[425px]">
56+
<DialogHeader>
57+
<DialogTitle>Choose Match Preferences</DialogTitle>
58+
</DialogHeader>
59+
<div className="grid gap-4 py-4">
60+
<div className="flex items-center gap-4">
61+
<label htmlFor="difficulty" className="text-right">
62+
Difficulty:
63+
</label>
64+
<FilterSelect
65+
placeholder="Difficulty"
66+
options={DIFFICULTY_OPTIONS}
67+
onChange={(value) => setDifficulty(value)}
68+
value={difficulty}
69+
showSelectedValue={true}
70+
/>
71+
</div>
72+
<div className="flex items-center gap-4">
73+
<label htmlFor="topics" className="text-right">
74+
Topics:
75+
</label>
76+
<TopicsPopover
77+
selectedTopics={selectedTopics}
78+
onChange={setSelectedTopics}
79+
/>
80+
</div>
81+
</div>
82+
<Button
83+
onClick={handleConfirm}
84+
disabled={!difficulty || selectedTopics.length === 0}
85+
>
86+
Confirm and Match
87+
</Button>
88+
</DialogContent>
89+
</Dialog>
90+
);
91+
}

0 commit comments

Comments
 (0)