Skip to content

Commit

Permalink
Merge pull request #54 from brendantwh/collab
Browse files Browse the repository at this point in the history
Collab session page
  • Loading branch information
brendantwh authored Oct 30, 2024
2 parents b3c4922 + 54b6d94 commit 97e6462
Show file tree
Hide file tree
Showing 148 changed files with 27,710 additions and 862 deletions.
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ services:
- "3001:3001"

zookeeper:
image: confluentinc/cp-zookeeper:7.0.1
image: confluentinc/cp-zookeeper:7.7.1
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ports:
Expand All @@ -48,7 +48,7 @@ services:
retries: 5

kafka:
image: confluentinc/cp-kafka:7.0.1
image: confluentinc/cp-kafka:7.7.1
ports:
- "9092:9092"
environment:
Expand Down
3 changes: 2 additions & 1 deletion frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-empty-object-type": "off"
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}
122 changes: 98 additions & 24 deletions frontend/app/(authenticated)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,39 @@ 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 { usePathname } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { deleteAllCookies, getCookie, getUsername, isUserAdmin } from "../utils/cookie-manager";
import { isTokenExpired } from "../utils/token-utils";
import { useNavigationConfirm } from "../hooks/useNavConfirm";

function AdminNav(pathname: string) {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

if (!isClient) return null;

if (isUserAdmin()) {
return (
<Link
href="/question-repo"
className={`text-lg font-semibold uppercase transition duration-100
${pathname === '/question-repo'
? 'text-gray-700 drop-shadow-md'
: 'text-gray-700/50 hover:text-gray-700'
}`}
prefetch={false}
>
Repository
</Link>
)
}

return (<></>);
}

export default function AuthenticatedLayout({
children,
Expand All @@ -14,6 +46,34 @@ export default function AuthenticatedLayout({
}>) {
const pathname = usePathname();

const router = useRouter();

const isSessionPage = pathname.startsWith('/session');
const handleNavigation = useNavigationConfirm(isSessionPage);

useEffect(() => {
const authenticateUser = async () => {
const token = getCookie('token');
if (!token || await isTokenExpired(token)) {
router.push('/auth/login');
return;
}

// if non-admin user tries to access repo, redirect user to question page
if (pathname.includes('/question-repo') && !isUserAdmin()) {
router.push('/questions');
return;
}
};

authenticateUser();
}, [pathname, router]);

function logout() {
deleteAllCookies();
router.push('/');
}

return (
<div className="min-h-screen bg-white relative">
<header className="flex items-center justify-between px-8 py-4 fixed top-0 left-0 right-0 z-10 bg-white/80 backdrop-blur-sm">
Expand All @@ -22,6 +82,10 @@ export default function AuthenticatedLayout({
href="/"
className="text-2xl font-bold font-brand tracking-tight text-brand-700"
prefetch={false}
onClick={(e) => {
e.preventDefault();
handleNavigation("/");
}}
>
PeerPrep
</Link>
Expand All @@ -31,29 +95,24 @@ export default function AuthenticatedLayout({
</Badge>
)}
</div>
<div className="hidden desktop:flex items-center gap-4">
<div className="flex items-center gap-4">
<nav className="flex items-center gap-10 font-brand">
<Link
href="/questions"
<Link
href="/questions"
className={`text-lg font-semibold uppercase transition duration-100
${pathname === '/questions'
? 'text-gray-700 drop-shadow-md'
: 'text-gray-700/50 hover:text-gray-700'
}`} prefetch={false}
>
Questions
</Link>
<Link
href="/question-repo"
className={`text-lg font-semibold uppercase transition duration-100
${pathname === '/question-repo'
? 'text-gray-700 drop-shadow-md'
${pathname === '/questions'
? 'text-gray-700 drop-shadow-md'
: 'text-gray-700/50 hover:text-gray-700'
}`}
}`}
onClick={(e) => {
e.preventDefault();
handleNavigation("/questions");
}}
prefetch={false}
>
Repository
Questions
</Link>
{AdminNav(pathname)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
Expand All @@ -65,14 +124,29 @@ export default function AuthenticatedLayout({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="font-sans">
<DropdownMenuLabel>Username</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild><Link href="/profile" className="cursor-pointer"><User className="mr-2 h-4 w-4" />Profile</Link></DropdownMenuItem>
{!pathname.includes('/profile') && (<><DropdownMenuLabel>{getUsername()}</DropdownMenuLabel>
<DropdownMenuSeparator /></>)}
<DropdownMenuItem asChild>
<Link href="/profile" className="cursor-pointer"
onClick={(e) => {
e.preventDefault();
handleNavigation("/profile");
}}
>
<User className="mr-2 h-4 w-4" />Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild onClick={(e) => {
// e.preventDefault();
localStorage.removeItem("token");
logout();
}}>
<Link href="/" className="cursor-pointer"><LogOut className="mr-2 h-4 w-4" />Log out</Link>
<Link href="/" className="cursor-pointer"
onClick={(e) => {
e.preventDefault();
handleNavigation("/");
}}
>
<LogOut className="mr-2 h-4 w-4" />Log out
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
16 changes: 9 additions & 7 deletions frontend/app/(authenticated)/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { deleteCookie, getCookie, setCookie } from "@/app/utils/cookie-manager";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
Expand Down Expand Up @@ -28,7 +29,7 @@ export default function Home() {
useEffect(() => {
const authenticateUser = async () => {
try {
const token = localStorage.getItem('token');
const token = getCookie('token');

if (!token) {
router.push('/auth/login'); // Redirect to login if no token
Expand All @@ -44,7 +45,7 @@ export default function Home() {
});

if (!response.ok) {
localStorage.removeItem("token"); // remove invalid token from browser
deleteCookie('token'); // remove invalid token from browser
router.push('/auth/login'); // Redirect to login if not authenticated
return;
}
Expand All @@ -70,7 +71,7 @@ export default function Home() {
}
};
authenticateUser();
}, []);
}, [router]);

// Validate the password before making the API call
const validatePassword = (password: string) => {
Expand Down Expand Up @@ -102,7 +103,7 @@ export default function Home() {
method: "PATCH",
headers: {
'Content-Type': 'application/json',
'authorization': `Bearer ${localStorage.getItem('token')}`
'authorization': `Bearer ${getCookie('token')}`
},
body: JSON.stringify({ username: userData.username }),
});
Expand All @@ -123,6 +124,7 @@ export default function Home() {
throw new Error("User not found" + signUpResponse.statusText);
} else {
initialUserData.current.username = userData.username; // update username new value
setCookie('username', userData.username, { 'max-age': '86400', 'path': '/', 'SameSite': 'Strict' });
}
}
if (userData.email !== initialUserData.current.email) {
Expand Down Expand Up @@ -177,7 +179,7 @@ export default function Home() {
method: "PATCH",
headers: {
'Content-Type': 'application/json',
'authorization': `Bearer ${localStorage.getItem('token')}`,
'authorization': `Bearer ${getCookie('token')}`,
},
body: JSON.stringify({ password: userData.password }),
});
Expand Down Expand Up @@ -209,12 +211,12 @@ export default function Home() {
const response = await fetch(`${process.env.NEXT_PUBLIC_USER_API_AUTH_URL}/verify-token`, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Authorization': `Bearer ${getCookie('token')}`,
},
});

if (!response.ok) {
localStorage.removeItem("token"); // remove invalid token from browser
deleteCookie("token"); // remove invalid token from browser
router.push('/auth/login'); // Redirect to login if not authenticated
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import { z } from "zod";
Expand All @@ -28,7 +26,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { escape } from "querystring";

interface AddEditQuestionDialogProps {
row: Question | null;
Expand Down Expand Up @@ -72,13 +69,12 @@ function AddEditQuestionDialog(
resetError();
setReset(false);
}
}, [reset]);
}, [reset, setReset]);

// User input value
const [newQuestion, setNewQuestion] = useState({
id: row?.id || undefined,
title: row?.title || "",
// summary: row?.summary || "",
description: row?.description || "",
complexity: row?.complexity
? capitalizeFirstLetter(row?.complexity)
Expand Down Expand Up @@ -117,14 +113,14 @@ function AddEditQuestionDialog(
return !Object.values(newErrors).includes(true);
};

const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const apiUrl = process.env.NEXT_PUBLIC_QUESTION_API_BASE_URL;
async function createQuestion() {
// validate required fields
if (validateFields()) {
try {
const endpoint = row?.id ? `update/${row?.id}` : "add";
const method = row?.id ? "PUT" : "POST";
const response = await fetch(`${apiUrl}/questions/${endpoint}`, {
const response = await fetch(`${apiUrl}/${endpoint}`, {
method: method,
headers: {
"Content-Type": "application/json",
Expand Down Expand Up @@ -182,8 +178,6 @@ function AddEditQuestionDialog(
);
}
}

window.location.reload()
} else {
throw new Error(`Unexpected response format: ${responseText}`);
}
Expand Down
5 changes: 1 addition & 4 deletions frontend/app/(authenticated)/question-repo/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { AlignLeft } from "lucide-react"
export type Question = {
id: number,
title: string,
summary: string,
description: string,
categories: string[],
complexity: string,
Expand Down Expand Up @@ -118,9 +117,7 @@ export const columns: (param: Dispatch<SetStateAction<Question[]>>) => ColumnDef
// },
filterFn: (row, id, selectedCategories) => {
const rowCategories = row.getValue(id);
console.log(selectedCategories);
console.log(rowCategories);
return selectedCategories.every(category => rowCategories.includes(category));
return selectedCategories.every((category: string) => (rowCategories as string[]).includes(category));
},

},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface DataTableRowActionsProps<TData> {
setData?: React.Dispatch<React.SetStateAction<TData[]>>
}

export function DataTableRowActions<TData>({
export function DataTableRowActions<TData extends Question>({
row, setData
}: DataTableRowActionsProps<TData>) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
Expand Down Expand Up @@ -74,8 +74,8 @@ export function DataTableRowActions<TData>({
in this case we use this solution:
https://github.com/radix-ui/primitives/issues/1836#issuecomment-2177341164
*/}
<AddEditQuestionDialog row={row.original as Question} ref={triggerEditRef} reset={isDialogOpen} setReset={setIsDialogOpen} setData={setData} handleClose={handleEditClose}/>
<DelQuestionDialog row={row.original as Question} ref={triggerDelRef} setData={setData} handleClose={handleDelClose}/>
<AddEditQuestionDialog row={row.original as Question} ref={triggerEditRef} reset={isDialogOpen} setReset={setIsDialogOpen} setData={setData as React.Dispatch<React.SetStateAction<Question[]>> | undefined} handleClose={handleEditClose}/>
<DelQuestionDialog row={row.original as Question} ref={triggerDelRef} setData={setData as React.Dispatch<React.SetStateAction<Question[]>> | undefined} handleClose={handleDelClose}/>
</DropdownMenu>

)
Expand Down
Loading

0 comments on commit 97e6462

Please sign in to comment.