Skip to content

Commit

Permalink
Merge pull request #56 from Jiayan-Lim/questionHistory-ui
Browse files Browse the repository at this point in the history
User question history UI
  • Loading branch information
Jiayan-Lim authored Oct 31, 2024
2 parents 7d8205e + 6b1941f commit 4d46f55
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 14 deletions.
2 changes: 1 addition & 1 deletion backend/matching-service/Dockerfile.match
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:14
FROM node:16

WORKDIR /usr/src/app

Expand Down
4 changes: 2 additions & 2 deletions backend/user-service/routes/user-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion frontend/.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -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/users/history

# Matching service
MATCHING_API_PORT=3002
Expand Down
12 changes: 11 additions & 1 deletion frontend/app/(authenticated)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { deleteAllCookies, getCookie, getUsername, isUserAdmin } from "../utils/cookie-manager";
Expand Down Expand Up @@ -136,6 +136,16 @@ export default function AuthenticatedLayout({
<User className="mr-2 h-4 w-4" />Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/profile/question-history" className="cursor-pointer"
onClick={(e) => {
e.preventDefault();
handleNavigation("/profile/question-history");
}}
>
<FileText className="mr-2 h-4 w-4" />Attempt History
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild onClick={(e) => {
logout();
}}>
Expand Down
42 changes: 38 additions & 4 deletions frontend/app/(authenticated)/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@ 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({
username: "johndoe",
email: "[email protected]",
password: "abcdefgh",
totalAttempt: 0,
questionAttempt: 0,
totalQuestion: 20,
});
const initialUserData = useRef({
username: "johndoe",
email: "[email protected]",
password: "abcdefgh",
totalAttempt: 0,
questionAttempt: 0,
totalQuestion: 20,
})
const userId = useRef(null);

Expand Down Expand Up @@ -51,25 +58,49 @@ 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 = "";

// 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) {
setError(true);
}

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,
})
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) {
console.error('Error during authentication:', error);
router.push('/auth/login'); // Redirect to login in case of any error
}
};

authenticateUser();
}, [router]);

Expand Down Expand Up @@ -302,20 +333,23 @@ export default function Home() {
onChange={handleInputChange}
/>
</div>

{ !error &&
<div className="flex justify-between">
<div className="text-left">
<Label>Questions Attempted</Label>
<div className="flex items-end gap-1.5 leading-7 font-mono">
<span className="text-2xl font-bold">11</span>
<span className="text-2xl font-bold">{userData.questionAttempt}</span>
<span>/</span>
<span>20</span>
<span>{userData.totalQuestion}</span>
</div>
</div>
<div className="text-right">
<Label>Total Attempts</Label>
<p className="text-2xl font-bold font-mono">14</p>
<p className="text-2xl font-bold font-mono">{userData.totalAttempt}</p>
</div>
</div>
}
</div>
</CardContent>
</Card>
Expand Down
172 changes: 172 additions & 0 deletions frontend/app/(authenticated)/profile/question-history/columns.tsx
Original file line number Diff line number Diff line change
@@ -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<QuestionHistory>[]= [
{
accessorKey: "id",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
ID
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
},
{
accessorKey: "title",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Title
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
return (
<HoverCard>
<HoverCardTrigger>
<div className="flex space-x-2">
<span className="font-medium cursor-help">
{row.getValue("title")}
</span>
</div>
</HoverCardTrigger>
<HoverCardContent className="rounded-xl">
<div className="flex flex-col">
<div className="flex items-center font-semibold mb-2">
<AlignLeft className="h-4 w-4 mr-2" />
<span>Description</span>
</div>
<div>
<p>{row.original.description}</p>
</div>
</div>
</HoverCardContent>
</HoverCard>
)
},
},
{
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Categories
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
accessorKey: "categories",
cell: ({ row }) => (
<div className="w-[140px]">
{row.original.categories.map((category, index) => (
<Badge key={index} variant="category" className="mr-1 my-0.5">
{category}
</Badge>
))}
</div>
),
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 (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Complexity
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
accessorKey: "complexity",
cell: ({ row }) => (
<div className="w-[40px]">
<Badge variant={row.original.complexity.toLowerCase() as BadgeProps["variant"]}>
{row.original.complexity}
</Badge>
</div>
),
filterFn: (row, id, value) => {
return value.includes(row.getValue(id))
},
},
{
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Number of Attempts
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
accessorKey: "attemptCount",
cell: ({ row }) => <div className="flex items-center justify-center h-full">{row.getValue("attemptCount")}</div>,
},
{
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Total Time Spent (mins)
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
accessorKey: "attemptTime",
cell: ({ row }) => <div className="flex items-center justify-center h-full">{ Math.floor(row.getValue("attemptTime")/60)}</div>,
// Cell: ({ value }) => Math.floor(value / 60), // Convert time spent in seconds to minutes
},
{
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Last Attempted
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
accessorKey: "attemptDate",
cell: ({ row }) => row.getValue("attemptDate").toLocaleString(),
},
];
Loading

0 comments on commit 4d46f55

Please sign in to comment.