Skip to content

Commit

Permalink
Merge branch 'enhancement/question-cat-enum' of github.com:CS3219-AY2…
Browse files Browse the repository at this point in the history
…425S1/cs3219-ay2425s1-project-g50 into enhancement/question-cat-enum
  • Loading branch information
jq1836 committed Oct 19, 2024
2 parents f412b3b + d423ffa commit dc56924
Show file tree
Hide file tree
Showing 15 changed files with 387 additions and 220 deletions.
13 changes: 13 additions & 0 deletions frontend/app/app/matching/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import AuthPageWrapper from "@/components/auth/auth-page-wrapper";
import FindMatch from "@/components/matching/find-match";
import { Suspense } from "react";

export default function MatchingPage() {
return (
<AuthPageWrapper requireLoggedIn>
<Suspense>
<FindMatch />
</Suspense>
</AuthPageWrapper>
);
}
2 changes: 1 addition & 1 deletion frontend/app/app/user-settings/[user_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function UserSettingsPage({
}) {
return (
<AuthPageWrapper requireLoggedIn userId={params.user_id}>
<UserSettings userId={params.user_id} />;
<UserSettings userId={params.user_id} />
</AuthPageWrapper>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "@/components/ui/table";
import LoadingScreen from "@/components/common/loading-screen";
import AdminEditUserModal from "@/components/admin-user-management/admin-edit-user-modal";
import DeleteAccountModal from "@/components/common/delete-account-modal";
import { PencilIcon, Trash2Icon } from "lucide-react";
import { User, UserArraySchema } from "@/lib/schemas/user-schema";
import { userServiceUri } from "@/lib/api/api-uri";
Expand Down Expand Up @@ -51,25 +52,33 @@ export default function AdminUserManagement() {
const [users, setUsers] = useState<User[]>([]);
const [showModal, setShowModal] = useState<boolean>(false);
const [selectedUser, setSelectedUser] = useState<User>();
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [confirmUsername, setConfirmUsername] = useState("");
const [isDeleteButtonEnabled, setIsDeleteButtonEnabled] = useState(false);

useEffect(() => {
if (data) {
setUsers(data);
}
}, [data]);

// Enable delete button in the delete account modal only when the input username matches the original username
useEffect(() => {
setIsDeleteButtonEnabled(confirmUsername === selectedUser?.username);
}, [confirmUsername, selectedUser]);

if (isLoading) {
return <LoadingScreen />;
}

const handleDelete = async (userId: string) => {
const handleDelete = async () => {
const token = auth?.token;
if (!token) {
throw new Error("No authentication token found");
}

const response = await fetch(
`${userServiceUri(window.location.hostname)}/users/${userId}`,
`${userServiceUri(window.location.hostname)}/users/${selectedUser?.id}`,
{
method: "DELETE",
headers: {
Expand All @@ -82,7 +91,7 @@ export default function AdminUserManagement() {
throw new Error("Failed to delete user");
}

setUsers(users.filter((user) => user.id !== userId));
setUsers(users.filter((user) => user.id !== selectedUser?.id));
};

const onUserUpdate = () => {
Expand All @@ -100,6 +109,16 @@ export default function AdminUserManagement() {
user={selectedUser}
onUserUpdate={onUserUpdate}
/>
<DeleteAccountModal
showDeleteModal={showDeleteModal}
originalUsername={selectedUser?.username || ""}
confirmUsername={confirmUsername}
setConfirmUsername={setConfirmUsername}
handleDeleteAccount={handleDelete}
isDeleteButtonEnabled={isDeleteButtonEnabled}
setShowDeleteModal={setShowDeleteModal}
isAdmin={true}
/>
<Table>
<TableHeader>
<TableRow>
Expand Down Expand Up @@ -130,7 +149,10 @@ export default function AdminUserManagement() {
</Button>
<Button
variant="destructive"
onClick={() => handleDelete(user.id)}
onClick={() => {
setSelectedUser(user);
setShowDeleteModal(true);
}}
>
<Trash2Icon className="h-4 w-4" />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface DeleteAccountModalProps {
handleDeleteAccount: () => void;
isDeleteButtonEnabled: boolean;
setShowDeleteModal: (show: boolean) => void;
isAdmin: boolean;
}

const DeleteAccountModal: React.FC<DeleteAccountModalProps> = ({
Expand All @@ -27,6 +28,7 @@ const DeleteAccountModal: React.FC<DeleteAccountModalProps> = ({
handleDeleteAccount,
isDeleteButtonEnabled,
setShowDeleteModal,
isAdmin,
}) => {
return (
<>
Expand All @@ -37,10 +39,18 @@ const DeleteAccountModal: React.FC<DeleteAccountModalProps> = ({
<DialogTitle>Confirm Delete Account</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p>To confirm, please type your username ({originalUsername}):</p>
{isAdmin ? (
<p>
To delete, please confirm the username ({originalUsername}):
</p>
) : (
<p>
To confirm, please type your username ({originalUsername}):
</p>
)}
<Input
type="text"
placeholder="Enter your username"
placeholder="Confirm username"
value={confirmUsername}
onChange={(e) => setConfirmUsername(e.target.value)}
/>
Expand All @@ -57,7 +67,11 @@ const DeleteAccountModal: React.FC<DeleteAccountModalProps> = ({
</Button>
<Button
variant="destructive"
onClick={handleDeleteAccount}
onClick={() => {
setShowDeleteModal(false);
setConfirmUsername("");
handleDeleteAccount();
}}
disabled={!isDeleteButtonEnabled}
>
Delete Account
Expand Down
66 changes: 66 additions & 0 deletions frontend/components/matching/find-match.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";
import React, { useState, useEffect } from "react";
import { MatchForm } from "@/components/matching/matching-form";
import { SearchProgress } from "@/components/matching/search-progress";
import { SelectionSummary } from "@/components/matching/selection-summary";
import { useToast } from "@/components/hooks/use-toast";

export default function FindMatch() {
const [selectedDifficulty, setSelectedDifficulty] = useState<string>("");
const [selectedTopic, setSelectedTopic] = useState<string>("");
const [isSearching, setIsSearching] = useState<boolean>(false);
const [waitTime, setWaitTime] = useState<number>(0);
const { toast } = useToast();

useEffect(() => {
let interval: NodeJS.Timeout | undefined;
if (isSearching) {
interval = setInterval(() => {
setWaitTime((prevTime) => prevTime + 1);
}, 1000);
} else {
setWaitTime(0);
}
return () => clearInterval(interval);
}, [isSearching]);

const handleSearch = () => {
if (selectedDifficulty && selectedTopic) {
setIsSearching(true);
} else {
toast({
title: "Invalid Selection",
description: "Please select both a difficulty level and a topic",
variant: "destructive",
});
}
};

const handleCancel = () => {
setIsSearching(false);
setWaitTime(0);
};

return (
<div className="container mx-auto p-4">
<MatchForm
selectedDifficulty={selectedDifficulty}
setSelectedDifficulty={setSelectedDifficulty}
selectedTopic={selectedTopic}
setSelectedTopic={setSelectedTopic}
handleSearch={handleSearch}
isSearching={isSearching}
handleCancel={handleCancel}
/>

{isSearching && <SearchProgress waitTime={waitTime} />}

{!isSearching && (selectedDifficulty || selectedTopic) && (
<SelectionSummary
selectedDifficulty={selectedDifficulty}
selectedTopic={selectedTopic}
/>
)}
</div>
);
}
115 changes: 115 additions & 0 deletions frontend/components/matching/matching-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";

const difficulties: string[] = ["Easy", "Medium", "Hard"];
const topics: string[] = [
"Arrays",
"Strings",
"Linked Lists",
"Trees",
"Graphs",
"Dynamic Programming",
];

interface MatchFormProps {
selectedDifficulty: string;
setSelectedDifficulty: (difficulty: string) => void;
selectedTopic: string;
setSelectedTopic: (topic: string) => void;
handleSearch: () => void;
isSearching: boolean;
handleCancel: () => void;
}

export function MatchForm({
selectedDifficulty,
setSelectedDifficulty,
selectedTopic,
setSelectedTopic,
handleSearch,
isSearching,
handleCancel,
}: MatchFormProps) {
return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle>Find a Match</CardTitle>
<CardDescription>
Select your preferred difficulty level and topic to find a match.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<Label
htmlFor="difficulty-select"
className="text-lg font-medium mb-2 block"
>
Difficulty Level
</Label>
<Select
value={selectedDifficulty}
onValueChange={setSelectedDifficulty}
>
<SelectTrigger id="difficulty-select">
<SelectValue placeholder="Select difficulty" />
</SelectTrigger>
<SelectContent>
{difficulties.map((difficulty) => (
<SelectItem key={difficulty} value={difficulty}>
{difficulty}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label
htmlFor="topic-select"
className="text-lg font-medium mb-2 block"
>
Topic
</Label>
<Select value={selectedTopic} onValueChange={setSelectedTopic}>
<SelectTrigger id="topic-select">
<SelectValue placeholder="Select topic" />
</SelectTrigger>
<SelectContent>
{topics.map((topic) => (
<SelectItem key={topic} value={topic}>
{topic}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between">
{!isSearching ? (
<Button onClick={handleSearch}>Find Match</Button>
) : (
<Button variant="destructive" onClick={handleCancel}>
Cancel Search
</Button>
)}
</CardFooter>
</Card>
);
}
42 changes: 42 additions & 0 deletions frontend/components/matching/search-progress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Clock } from "lucide-react";

interface SearchProgressProps {
waitTime: number;
}

export function SearchProgress({ waitTime }: SearchProgressProps) {
return (
<Card className="w-full max-w-2xl mx-auto mt-4">
<CardHeader>
<CardTitle>Searching for Match</CardTitle>
<CardDescription>
Please wait while we find a suitable match for you.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<Progress className="w-full" indeterminate />
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4" />
<span>
Wait Time: {Math.floor(waitTime / 60)}:
{(waitTime % 60).toString().padStart(2, "0")}
</span>
</div>
<span>Searching...</span>
</div>
</div>
</CardContent>
</Card>
);
}
Loading

0 comments on commit dc56924

Please sign in to comment.